Message hooks
These hooks build the handlers of one message: its actions, its thread, its reactions, its mentions and its author. They're exported from stream-chat-react.
Message calls them for the message it renders and puts the results in MessageContext. Inside a custom MessageUI, read the handlers from useMessageContext() instead of calling the hooks again:
| Hook | In MessageContext |
|---|---|
useActionHandler |
handleAction |
useDeleteHandler |
handleDelete |
useFlagHandler |
handleFlag |
useMarkUnreadHandler |
handleMarkUnread |
useMuteHandler |
handleMute |
usePinHandler |
handlePin |
useRetryHandler |
handleRetry, which takes the message |
useOpenThreadHandler |
handleOpenThread |
useReactionHandler |
handleReaction |
useReactionsFetcher |
handleFetchReactions |
useMentionsHandler |
onMentionsClickMessage and onMentionsHoverMessage |
useUserHandler |
onUserClick and onUserHover |
The context's other values are on MessageContext, and the default actions that call these handlers on Actions. Two more hooks for a message have pages of their own: useMessageReminder on Reminder hooks, and useMessageTextStreaming on AI hooks.
Each entry gives the signature, then:
- Returns: what the hook gives back.
- Reads: the store or the context it reads.
- Re-renders: what makes the component render again.
- Needs: where the component must sit, and what the hook does outside that place.
useDeleteHandler
Returns a function that deletes the message.
useDeleteHandler(message?: LocalMessage, notifications?: DeleteMessageNotifications): (options?: DeleteMessageOptions) => Promise<void> | void- Returns: a function that deletes the message with
MessageOperations.delete(), the thread's inside aThread, otherwise the channel's. The message shows as deleted at once, and comes back if the server refuses.optionsare the request's,{ hard?, delete_for_me? }. The function doesn't reject: on an error it callsnotifications.notifywhen you pass one, and resolves. - Reads: no store.
- Re-renders: never by itself.
- Needs:
Chat, and aChannelor aThread. Throws outside them.
What a delete changes before the server answers is on What's optimistic and what isn't.
useFlagHandler
Returns a click handler that flags the message for moderation.
useFlagHandler(message?: LocalMessage): ReactEventHandler- Returns:
(event) => Promise<void>, which callsclient.moderation.flagMessage({ message_id: message.id }). It rejects when the request fails. - Reads: no store.
- Re-renders: never by itself.
- Needs:
Chat. Throws outside it.
useMarkUnreadHandler
Returns a click handler that marks the channel unread from the message on.
useMarkUnreadHandler(message?: LocalMessage, notifications?: MarkUnreadHandlerNotifications): ReactEventHandler- Returns:
(event) => Promise<void>, which callsChannel.markUnread({ message_id }). It rejects when the request fails. After a success it callsnotifications.notifywith the text ofgetSuccessNotification, when you pass both. - Reads: no store. The read state changes when the
notification.mark_unreadevent arrives. - Re-renders: never by itself.
- Needs: a
Channelor aThread. Throws outside both.
useMuteHandler
Returns a click handler that mutes the message's author, or unmutes them when they're muted.
useMuteHandler(message?: LocalMessage, notifications?: MuteUserNotifications): ReactEventHandler- Returns:
(event) => Promise<void>. It checksclient.mutedUsersStoreand callsclient.moderation.mute()orclient.moderation.unmute()formessage.user. It doesn't reject: on success or error it callsnotifications.notifywhen you pass one, and resolves. - Reads:
client.mutedUsersStore, once per click. - Re-renders: never by itself.
- Needs:
Chat. Throws outside it.
usePinHandler
Returns a click handler that pins the message, or unpins it when it's pinned.
usePinHandler(message: LocalMessage, notifications?: PinMessageNotifications): { handlePin: ReactEventHandler }- Returns:
{ handlePin }.handlePin(event)shows the message as pinned or unpinned at once, then callsclient.pinMessage()orclient.unpinMessage(). On an error it puts the message back as it was and callsnotifications.notifywhen you pass one. It doesn't reject. - Reads: no store. It writes to the message list of the thread or the channel.
- Re-renders: never by itself.
- Needs:
Chat, and aChannelor aThread. Throws outside them.
useRetryHandler
Returns a function that sends a failed message again.
useRetryHandler(): RetryHandler- Returns:
({ localMessage, options? }) => Promise<void>, which callsMessageOperations.retry()of the thread or the channel. It resends the request that failed, and rejects when the retry fails too. The message stays in the list either way, markedfailedagain after a failure. - Reads: no store.
- Re-renders: when the
Threador theChannelabove changes. - Needs: a
Channelor aThread. Throws outside both.
It takes no message: pass it when you call the function. handleRetry in MessageContext takes the message itself and calls this function with { localMessage: message }.
useActionHandler
Returns the handler for the buttons of a message with actions, such as the Send, Shuffle and Cancel buttons of a /giphy preview.
useActionHandler(message?: LocalMessage): ActionHandlerReturnType- Returns:
(dataOrName?, value?, event?) => Promise<void> | void. It sends the chosen action withChannel.sendAction(message.id, data). Pass the action'snameandvalue, or an object of them, such as{ image_action: "shuffle" }. When the server returns a message, it replaces the one in the list; otherwise the message is removed. - Reads: no store. It writes to the message list of the thread or the channel.
- Re-renders: never by itself.
- Needs: a
Channelor aThread. Throws outside both.
Attachment receives it as actionHandler, and renders the buttons of the message's actions with it.
useOpenThreadHandler
Returns a click handler that opens the message's thread.
useOpenThreadHandler(message?: LocalMessage, customOpenThread?: (message: LocalMessage, event: React.BaseSyntheticEvent) => void): ReactEventHandler- Returns:
(event) => void. It callscustomOpenThreadwhen you pass one. Otherwise it callsopenThread({ channel, message })fromuseWorkspaceNavigation(). - Reads: the channel from
useChannel(), and the workspace navigation. No store. - Re-renders: when the channel or the workspace navigation changes.
- Needs: a
Channelor aThread. Throws outside both. Without a layout that opens threads, such asChatView, the default does nothing: passcustomOpenThreadto open the thread yourself.
Where a thread opens is on Moving between channels, threads and messages.
useMessageAlsoSentInChannelNavigation
Returns the navigation behind the "Also sent in channel" and "Replied to a thread" labels of a reply.
useMessageAlsoSentInChannelNavigation(): MessageAlsoSentInChannelNavigation- Returns:
viewReference(): in a thread, jumps to the reply in the channel. In a channel, opens the reply's thread and jumps to the reply there.viewReplyInChannel(messageId?)andviewReplyInThread(replyId?, parentId?): each half on its own. Both default to the message in context.isInThread: whether the component renders inside aThread.isShownInChannel: whether the reply was also sent to the channel,message.show_in_channel.
- Reads: the message, channel, thread and workspace navigation contexts. No store.
- Re-renders: when one of those contexts changes.
- Needs:
Chat, aChannelor aThread, and aMessage. Throws outside them.
When the thread can't be loaded, it publishes an api:message:search:not-found notification. MessageAlsoSentInChannelIndicator uses it.
useReactionHandler
Returns a function that adds a reaction, or removes it when the current user has already reacted with that type.
useReactionHandler(message?: LocalMessage): (reactionType: string, event?: React.BaseSyntheticEvent) => Promise<void>- Returns: a function that takes a reaction type such as
"like". It shows the change at once, then callsclient.sendReaction()orclient.deleteReaction(). On an error it puts the message back as it was, and resolves. - Reads:
reactionOptionsfromComponentContext, for the emoji code sent with the reaction. It writes to the message list of the thread or the channel. - Re-renders: never by itself.
- Needs:
Chat, and aChannelor aThread. Throws outside them.
useReactionsFetcher
Returns a function that loads the reactions of the message.
useReactionsFetcher(message: LocalMessage): (reactionType?: string, sort?: SortParamRequest[]) => Promise<ReactionResponse[]>- Returns: a function that loads the message's reactions with
client.queryReactions(), 25 a request, up to 1000. Pass a type to load only that type. It rejects when a request fails. - Reads: no store. The reactions aren't kept: each call loads them again.
- Re-renders: never by itself.
- Needs:
Chat. Throws outside it.
MessageReactionsDetail, the list of who reacted, calls it as handleFetchReactions.
useMentionsHandler
Returns click and hover handlers for the mentions in the message's text.
useMentionsHandler(message?: LocalMessage, customMentionHandler?: { onMentionsClick?: CustomMentionHandler; onMentionsHover?: CustomMentionHandler }): { onMentionsClick: ReactEventHandler; onMentionsHover: ReactEventHandler }- Returns:
onMentionsClickandonMentionsHover. Each calls your handler with(event, mentionedUsers, message), and only when the message mentions someone: users, roles, groups, the channel or@here. Without your handler they do nothing. - Reads: no store.
- Re-renders: never by itself.
- Needs: nothing.
Message passes its onMentionsClick and onMentionsHover props here.
useUserHandler
Returns click and hover handlers for the message's author.
useUserHandler(message?: LocalMessage, eventHandlers?: { onUserClickHandler?: UserEventHandler; onUserHoverHandler?: UserEventHandler }): { onUserClick: ReactEventHandler; onUserHover: ReactEventHandler }- Returns:
onUserClickandonUserHover. Each calls your handler with(event, message.user). Without your handler, or a message without a user, they do nothing. - Reads: no store.
- Re-renders: never by itself.
- Needs: nothing.
Message passes its onUserClick and onUserHover props here. The default MessageUI attaches the results to the author's avatar.
useUserRole
Returns what the current user may do with the message.
useUserRole(message: LocalMessage, disableQuotedMessages?: boolean): { canBlockUser: boolean; canDelete: boolean; canEdit: boolean; canFlag: boolean; canMarkUnread: boolean; canMute: boolean; canPin: boolean; canQuote: boolean; canReact: boolean; canReply: boolean; canSendMessage: boolean; isMyMessage: boolean; isAdmin: boolean; isModerator: boolean; isOwner: boolean }- Returns: a flag for each action, worked out from the channel's capabilities and the message's author:
canEdit:update-any-message, orupdate-own-messageon your own message. Never for a poll or a/giphymessage.canDelete:delete-any-message, ordelete-own-messageon your own message.canFlag,canMuteandcanMarkUnread: only on someone else's message, withflag-message,mute-channelandread-events.canBlockUser: on someone else's message.canPin,canReact,canReplyandcanSendMessage:pin-message,send-reaction,send-replyandsend-message.canQuote:quote-message, unlessdisableQuotedMessagesistrue.isMyMessage: whether the current user wrote it.isAdmin,isModeratorandisOwnerare deprecated: they read roles, not capabilities.
- Reads:
channel.state, fieldownCapabilities, throughuseChannelCapabilities. - Re-renders: when the channel's capabilities change.
- Needs:
Chat, and aChannelor aThread. Throws outside them.
useBaseMessageActionSetFilter
Removes from an action set the actions that don't apply to the message in context.
useBaseMessageActionSetFilter(messageActionSet: MessageActionSetItem[], disable?: boolean): MessageActionSetItem[]- Returns: the actions that apply. It drops every action for a bounced, system, ephemeral or sending message. Otherwise it drops each action the user may not use according to
useUserRole, the ones that don't work on a thread reply, the ones with nothing to act on, such as Copy on a message without text, and Remind me and Save for later when reminders are off. A failed message keeps only Resend, Edit and Delete. Withdisable, it returns the set as it is. - Reads: the message from
useMessageContext(),useUserRole(), andchannel.configStatefor whether reminders are on. - Re-renders: when the message, the capabilities or the reminder setting changes.
- Needs:
Chat, aChannelor aThread, and aMessage. Throws outside them.
MessageActions runs it on its messageActionSet. Message actions explains when to run it yourself.
Best practices
- Inside a message, take the handlers from
useMessageContext(). Call a hook yourself only in a component that replacesMessage, or that has a message but noMessageContext. - Show an action only when
useUserRoleallows it, so a user never sees a button the server would refuse. - Wrap
handleFlag,handleMarkUnreadand retries intry/catch, and report the outcome withuseNotificationApi, as the default Flag and Mark unread actions do. Delete, mute, pin and reactions catch their own errors and resolve either way, so acatcharound them never runs. - Edit a message in the composer: call
MessageComposer.initState({ composition: message })on the composer fromuseMessageComposerController(). There's no edit hook.
Examples
Mark unread with a notification
import {
useMessageContext,
useNotificationApi,
useUserRole,
} from "stream-chat-react";
export const MarkUnreadButton = () => {
const { handleMarkUnread, message } = useMessageContext();
const { canMarkUnread } = useUserRole(message);
const { addNotification } = useNotificationApi();
if (!canMarkUnread) return null;
return (
<button
onClick={async (event) => {
try {
await handleMarkUnread(event);
} catch (error) {
addNotification({
emitter: "MarkUnreadButton",
error: error instanceof Error ? error : undefined,
message: "The message could not be marked unread",
severity: "error",
type: "api:message:markUnread:failed",
});
}
}}
type="button"
>
Mark unread
</button>
);
};Reaction buttons
import { useMessageContext } from "stream-chat-react";
export const QuickReactions = () => {
const { handleReaction } = useMessageContext();
return (
<>
<button onClick={(event) => handleReaction("love", event)} type="button">
Love
</button>
<button onClick={(event) => handleReaction("like", event)} type="button">
Like
</button>
</>
);
};Use the types of your reaction options. The defaults are haha, like, love, sad, wow and fire: see Reactions.
Retry a failed message
import type { LocalMessage } from "stream-chat";
import { useRetryHandler } from "stream-chat-react";
export const RetryButton = ({ message }: { message: LocalMessage }) => {
const retry = useRetryHandler();
if (message.status !== "failed") return null;
return (
<button
onClick={() => retry({ localMessage: message }).catch(() => undefined)}
type="button"
>
Retry
</button>
);
};A failed retry leaves the message marked failed, so the button stays.
Actions a user may take
import { useMessageContext, useUserRole } from "stream-chat-react";
export const MessagePermissions = () => {
const { message } = useMessageContext();
const { canDelete, canEdit, canReply } = useUserRole(message);
return (
<ul>
{canEdit && <li>Edit</li>}
{canDelete && <li>Delete</li>}
{canReply && <li>Reply</li>}
</ul>
);
};The Custom message recipe builds a whole MessageUI on these handlers.