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". How it works across the layers explains what it sends and how failures and repeated calls are handled. - Reads:
reactionOptionsfromComponentContext, for the emoji code sent with the reaction. - 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 }CustomMentionHandler is (event: React.BaseSyntheticEvent, message: LocalMessage) => void.
- Returns:
onMentionsClickandonMentionsHover. Each isundefinedwhen you don't pass the matching handler. Each calls your handler with(event, message), and only when the message mentions someone: users, roles, groups, the channel or@here.messageHasMentions(message)makes the same check. Each keeps its identity across renders and calls your latest handler. - 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.
useMessagePermissions
Returns what the current user may do with the message.
useMessagePermissions(message: LocalMessage): { canBlockUser: boolean; canDelete: boolean; canEdit: boolean; canFlag: boolean; canMarkUnread: boolean; canMute: boolean; canPin: boolean; canQuote: boolean; canReact: boolean; canReply: boolean; canSendMessage: boolean; isMyMessage: 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. This is only the permission, so it'struefor a poll message or a giphy too. A custom edit button that should hide on them checks!message.poll_id && message.command !== "giphy"too, as the defaulteditaction'sisVisibledoes.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.isMyMessage: whether the current user wrote it.
- Reads:
channel.state, fieldownCapabilities, throughuseChannelCapabilities. - Re-renders: when the channel's capabilities change.
- Needs:
Chat, and aChannelor aThread. Throws outside them.
useIsRenderedAsThreadParent
Tells whether the message in MessageContext is the parent message of the thread in ThreadContext. Use it in a message UI to render a thread's parent message differently from its replies.
useIsRenderedAsThreadParent(): boolean- Returns:
truefor the parent message of the thread the message is rendered in.falsefor the thread's replies, and for every message outside a thread, including a channel message that has replies. - Reads: the message from
useMessageContext()and the thread fromuseThreadContext(). - Re-renders: when the message or the thread in context changes.
- Needs: a
Message. Throws outside it, since it readsuseMessageContext().
A thread's id is its parent message's id, so the hook compares the two.
useBaseMessageActionSetFilter uses it to give the parent message no actions, so a ThreadHead override that renders <Message {...props} /> gets the same behavior.
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, and for the parent message of the thread it is rendered in, as
useIsRenderedAsThreadParent()tells. Otherwise it drops each action the user may not use according touseMessagePermissions, the ones that don't work on a thread reply, and Remind me and Save for later when reminders are off. A failed message keeps only Resend, Edit and Delete. Then it drops each action whose itemisVisiblereturnsfalse, such as Copy on a message without text. Withdisable, it skips every check except the items'isVisible. - Reads: the message from
useMessageContext(), the thread fromuseThreadContext(),useMessagePermissions(), andchannel.configStatefor whether reminders are on. - Re-renders: when the message, the thread, 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, and Rules on the items explains isVisible.
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
useMessagePermissionsallows 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,
useMessagePermissions,
useNotificationApi,
} from "stream-chat-react";
export const MarkUnreadButton = () => {
const { handleMarkUnread, message } = useMessageContext();
const { canMarkUnread } = useMessagePermissions(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, useMessagePermissions } from "stream-chat-react";
export const MessagePermissions = () => {
const { message } = useMessageContext();
const { canDelete, canEdit, canReply } = useMessagePermissions(message);
return (
<ul>
{canEdit && <li>Edit</li>}
{canDelete && <li>Delete</li>}
{canReply && <li>Reply</li>}
</ul>
);
};The Action bar for a custom message recipe builds a message's actions on these handlers and useMessagePermissions().