Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

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". It shows the change at once, then calls client.sendReaction() or client.deleteReaction(). On an error it puts the message back as it was, and resolves.
  • Reads: reactionOptions from ComponentContext, 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 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 }
  • Returns: onMentionsClick and onMentionsHover. 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: 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.

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, or update-own-message on your own message. Never for a poll or a /giphy message.
    • 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, unless disableQuotedMessages is true.
    • isMyMessage: whether the current user wrote it.
    • isAdmin, isModerator and isOwner are deprecated: they read roles, not capabilities.
  • 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.

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. With disable, it returns the set as it is.
  • Reads: the message from useMessageContext(), useUserRole(), and channel.configState for whether reminders are on.
  • Re-renders: when the message, 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.

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 useUserRole 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,
  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.