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 a Thread, otherwise the channel's. The message shows as deleted at once, and comes back if the server refuses. options are the request's, { hard?, delete_for_me? }. The function doesn't reject: on an error it calls notifications.notify when you pass one, and resolves.
  • Reads: no store.
  • Re-renders: never by itself.
  • Needs: Chat, and a Channel or a Thread. 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 calls client.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 calls Channel.markUnread({ message_id }). It rejects when the request fails. After a success it calls notifications.notify with the text of getSuccessNotification, when you pass both.
  • Reads: no store. The read state changes when the notification.mark_unread event arrives.
  • Re-renders: never by itself.
  • Needs: a Channel or a Thread. 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 checks client.mutedUsersStore and calls client.moderation.mute() or client.moderation.unmute() for message.user. It doesn't reject: on success or error it calls notifications.notify when 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 calls client.pinMessage() or client.unpinMessage(). On an error it puts the message back as it was and calls notifications.notify when 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 a Channel or a Thread. Throws outside them.

useRetryHandler

Returns a function that sends a failed message again.

useRetryHandler(): RetryHandler
  • Returns: ({ localMessage, options? }) => Promise<void>, which calls MessageOperations.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, marked failed again after a failure.
  • Reads: no store.
  • Re-renders: when the Thread or the Channel above changes.
  • Needs: a Channel or a Thread. 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 with Channel.sendAction(message.id, data). Pass the action's name and value, 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 Channel or a Thread. 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 calls customOpenThread when you pass one. Otherwise it calls openThread({ channel, message }) from useWorkspaceNavigation().
  • Reads: the channel from useChannel(), and the workspace navigation. No store.
  • Re-renders: when the channel or the workspace navigation changes.
  • Needs: a Channel or a Thread. Throws outside both. Without a layout that opens threads, such as ChatView, the default does nothing: pass customOpenThread to 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?) and viewReplyInThread(replyId?, parentId?): each half on its own. Both default to the message in context.
    • isInThread: whether the component renders inside a Thread.
    • 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, a Channel or a Thread, and a Message. 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: reactionOptions from ComponentContext, for the emoji code sent with the reaction.
  • Re-renders: never by itself.
  • Needs: Chat, and a Channel or a Thread. 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: onMentionsClick and onMentionsHover. Each is undefined when 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: onUserClick and onUserHover. 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, or update-own-message on your own message. This is only the permission, so it's true for 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 default edit action's isVisible does.
    • canDelete: delete-any-message, or delete-own-message on your own message.
    • canFlag, canMute and canMarkUnread: only on someone else's message, with flag-message, mute-channel and read-events.
    • canBlockUser: on someone else's message.
    • canPin, canReact, canReply and canSendMessage: pin-message, send-reaction, send-reply and send-message.
    • canQuote: quote-message.
    • isMyMessage: whether the current user wrote it.
  • Reads: channel.state, field ownCapabilities, through useChannelCapabilities.
  • Re-renders: when the channel's capabilities change.
  • Needs: Chat, and a Channel or a Thread. 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: true for the parent message of the thread the message is rendered in. false for 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 from useThreadContext().
  • Re-renders: when the message or the thread in context changes.
  • Needs: a Message. Throws outside it, since it reads useMessageContext().

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 to useMessagePermissions, 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 item isVisible returns false, such as Copy on a message without text. With disable, it skips every check except the items' isVisible.
  • Reads: the message from useMessageContext(), the thread from useThreadContext(), useMessagePermissions(), and channel.configState for whether reminders are on.
  • Re-renders: when the message, the thread, the capabilities or the reminder setting changes.
  • Needs: Chat, a Channel or a Thread, and a Message. 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 replaces Message, or that has a message but no MessageContext.
  • Show an action only when useMessagePermissions allows it, so a user never sees a button the server would refuse.
  • Wrap handleFlag, handleMarkUnread and retries in try/catch, and report the outcome with useNotificationApi, as the default Flag and Mark unread actions do. Delete, mute, pin and reactions catch their own errors and resolve either way, so a catch around them never runs.
  • Edit a message in the composer: call MessageComposer.initState({ composition: message }) on the composer from useMessageComposerController(). 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().