Message hooks
These hooks give a component the message it renders and the handlers of that message: its actions, its operations, its receipts and its reactions. They're exported from stream-chat-react-native and stream-chat-expo.
Message builds the handlers once for the message it renders and puts them in MessageContext. Inside a custom message component, read them from useMessageContext() or useMessageActionHandlers() instead of building them again. The context's values are on MessageContext, and the default actions that call the handlers on Actions. Two more hooks for a message have pages of their own: useMessageReminder on Reminder hooks, and useStreamingMessage 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.
useMessageContext
Returns the message being rendered, and what Message worked out for it.
useMessageContext(): MessageContextValue- Returns, among others:
message: theLocalMessage, andchannel, its channel.isMyMessage,alignment,groupStyles,lastGroupMessageandshowAvatar: where and how the message sits in the list.images,videos,filesandotherAttachments: its attachments by kind.reactionsandhasReactions: its reactions.messageActionHandlers: the handlers behind the message's actions, the same objectuseMessageActionHandlers()returns.handleReaction(type): what tapping a reaction does. It calls thehandleReactionprop ofChannelorThread, then toggles the reaction. AselectReactionprop onChannelorThreadreplaces it.undefinedon a failed message or an error message.onOpenThread(),onPress,onLongPress,showMessageOverlay()anddismissOverlay(): what pressing the message does, and the message menu.
- Reads: the context
Messageprovides. No store. - Re-renders: when
Messageprovides a new value, which happens when the message or one of the values worked out from it changes. - Needs: a
Message. Outside one it returns an empty object, typed as the full value, so checkmessagebefore you use it there.
useMessagesContext
Returns the message settings and handlers set on Channel or Thread.
useMessagesContext(): MessagesContextValue- Returns: the props of
ChannelandThreadthat configure messages: thehandle*callbacks,messageActions,supportedReactions,myMessageTheme,messageContentOrderand the rest. - Reads: the context
ChannelandThreadprovide. No store. - Re-renders: when the channel changes, or one of a few of those props does, such as
messageContentOrder,myMessageThemeor the number ofsupportedReactions. A newhandle*callback ormessageActionsfunction after mount doesn't provide a new value. - Needs: a
Channelor aThread. Throws outside both.
The values are listed on MessagesContext.
useMessageActionHandlers
Returns the handlers behind the actions of the message being rendered.
useMessageActionHandlers(): MessageActionHandlerCallbacks-
Returns: one handler for each action. They act on the message in context and take no message argument:
Handler What it does handleCopyMessage()Copies the text, translated into the user's language when a translation exists, and publishes a clipboard:message:copy:*notification. Does nothing without text.handleDeleteMessage()Asks for confirmation, then deletes the message and publishes an api:message:delete:*notification.handleDeleteForMeMessage()Deletes the message for the current user only. No confirmation and no notification. Rejects when the request fails. handleEditMessage()Starts editing the message in the composer, once the keyboard is open. handleQuotedReplyMessage()Sets the message as the composer's quoted message. handleResendMessage()Sends a failed message again. Rejects when the retry fails. handleFlagMessage()Asks for confirmation, then flags the message with client.moderation.flagMessage({ message_id })and publishes anapi:message:flag:*notification.handleMarkUnreadMessage()Marks the channel unread from the message on, with Channel.markUnread({ message_id }), and publishes anapi:message:markUnread:*notification.handleTogglePinMessage()Pins the message, or unpins it when it's pinned, and publishes an api:message:pin:*orapi:message:unpin:*notification.handleToggleMuteUser()Mutes the author, or unmutes them when they're muted, and publishes an api:user:mute:*orapi:user:unmute:*notification.handleToggleBanUser()Bans the author with client.moderation.ban(), or unbans them whenmessage.user.bannedis set. No notification. Rejects when the request fails.handleToggleBlockUser(user)Blocks the user, or unblocks them when they're blocked. Logs a failure instead of rejecting. handleToggleReaction(type)Adds the reaction, or removes it when the current user has already reacted with that type. Logs a failure instead of rejecting. The handlers that publish a notification catch their own errors and publish a
failednotification, so they resolve either way. -
Reads:
messageActionHandlersfromuseMessageContext(). No store. The handlers read what they act on, such as the mute and block lists, when they run. -
Re-renders: when
Messageprovides a new context value. -
Needs:
Chat, aChannelor aThread, and aMessage. Outside aMessageit returnsundefined.
Message builds one set for each message, and the menu and any component that calls this hook share it, so calling it costs nothing. The handlers don't call the handle* props of Channel: the actions from useMessageActions() do. Message actions in your own components explains the difference.
useMessageActions
Returns the default actions of the message being rendered, as the menu shows them.
useMessageActions(): {
banUser: MessageActionType;
blockUser: MessageActionType;
copyMessage: MessageActionType;
deleteForMeMessage: MessageActionType;
deleteMessage: MessageActionType;
editMessage: MessageActionType;
flagMessage: MessageActionType;
handleReaction: (reactionType: string) => Promise<void>;
markUnread: MessageActionType;
muteUser: MessageActionType;
pinMessage: MessageActionType;
quotedReply: MessageActionType;
retry: MessageActionType;
threadReply: MessageActionType;
unpinMessage: MessageActionType;
}- Returns: one
MessageActionTypefor each default action: itsaction,title,iconandtype. Eachactioncalls thehandle*prop ofChannelorThreadfirst, as the menu does, then the handler fromuseMessageActionHandlers(). The titles are translated, and the mute, block and ban titles follow the author's current state. - Reads: the message, messages and channel contexts, and the author's mute state from
client.mutedUsersStore. - Re-renders: when the message changes, and when the author is muted or unmuted.
- Needs:
Chat, aChannelor aThread, and aMessage. Throws outside them.
The SDK calls it only while a message's menu is open, because building the actions translates every title and subscribes to the author's mute state. Call it in a custom menu, not in every message row. The fields of MessageActionType are on Actions.
useMessageOperations
Returns the operations that change messages and reactions in the list the component renders in.
useMessageOperations(): MessageOperations- Returns:
deleteMessage(message, options?): deletes the message.optionsis{ hard: true }or{ delete_for_me: true }. A message that failed to send is only removed from the list, because the server never had it.retrySendMessage(message): sends a failed message again.sendReaction(type, messageId)anddeleteReaction(type, messageId): add or remove the current user's reaction. A new reaction replaces the user's others whenenforceUniqueReactionis set onChannel.removeMessage({ id, parent_id? }): removes the message from the lists and from the offline database, without a request.updateMessage(message): puts a message into the lists without a request: the channel's, unless it's a reply not shown in the channel, and the open thread's when it's a reply.
- Reads: the
Channel,ThreadandChatcontexts. No store. Each operation picks its owner when it runs: the thread for a reply loaded in the open thread, otherwise the channel. - Re-renders: never by itself. The functions keep their identity.
- Needs:
Chat, and aChannelor aThread. Throws outside them.
The delete, retry and reaction operations go through messageOperations of the thread or the channel, which shows the change at once and rolls it back if the server refuses. They reject when the server refuses the request. sendReaction and deleteReaction throw before any request when the channel isn't initialized. Message operations explains what happens before the server answers.
useIsMessageReadByOthers
Says whether another member has read a message.
useIsMessageReadByOthers(params: { message?: LocalMessage }): boolean- Returns:
truewhen the message is at or before the furthest point any other member has read. The hook compares every message with that one point, so an older message never reports less than a newer one.falsewithout a message. - Reads:
channel.messageReceiptsTracker.snapshotStore, fieldlastReadRefByOthers, of the channel of theChannelorThread. - Re-renders: when the answer changes.
- Needs: a
Channelor aThread. Throws outside both.
MessageStatus uses it, and useIsMessageDeliveredToOthers, for the default read and delivery icons. The tracker is on Message receipts tracker, and the feature on Read state, unread and receipts.
useIsMessageDeliveredToOthers
Says whether a message has reached another member.
useIsMessageDeliveredToOthers(params: { message?: LocalMessage }): boolean- Returns:
truewhen the message is at or before the furthest point any other member has received.falsewithout a message. - Reads:
channel.messageReceiptsTracker.snapshotStore, fieldlastDeliveredRefByOthers. - Re-renders: when the answer changes.
- Needs: a
Channelor aThread. Throws outside both.
useMessageReadData
Returns the users who have read a message.
useMessageReadData(params: { message?: LocalMessage }): UserResponse[]- Returns: every user whose read position is at the message or later, the current user included.
[]without a message. - Reads:
channel.messageReceiptsTracker.snapshotStore, throughMessageReceiptsTracker.readersForMessage(). - Re-renders: whenever the tracker publishes, because each call builds a new list.
- Needs: a
Channelor aThread. Throws outside both.
Leave the current user out for a "Seen by" label. For a flag only, use useIsMessageReadByOthers, which re-renders less.
useMessageDeliveredData
Returns the users who have received a message.
useMessageDeliveredData(params: { message?: LocalMessage }): UserResponse[]- Returns: every user who has received the message, readers included, the current user included.
[]without a message. - Reads:
channel.messageReceiptsTracker.snapshotStore, throughMessageReceiptsTracker.deliveredForMessage(). - Re-renders: whenever the tracker publishes.
- Needs: a
Channelor aThread. Throws outside both.
useFetchReactions
Loads the reactions of a message, a page at a time, and keeps them current.
useFetchReactions(params: {
limit?: number;
message?: LocalMessage;
reactionType?: string;
sort?: SortParamRequest[];
}): {
loading: boolean;
loadNextPage: () => Promise<void>;
reactions: ReactionResponse[];
}limit is 25 by default. With reactionType, only the reactions of that type load.
- Returns:
reactions: the reactions loaded so far.loadNextPage(): loads the next page. It does nothing after the last one.loading:trueuntil the first page loads. It staystruewhen the request fails.
- Reads:
client.queryReactionsAndHydrate(), into a list the hook keeps for the component. It loads the first page when it mounts, and again when the message, the type or the sort changes. It followsreaction.new,reaction.updatedandreaction.deletedfor the message, and the reactions the offline database returns while offline. - Re-renders: when a page loads, and when a reaction event for the message arrives.
- Needs:
Chat. Throws outside it.
A failed request publishes an api:message:reactions:fetch:failed notification. Define sort at module scope: a new array on each render loads the first page again. The reactions screen of the message menu uses this hook. Reactions shows a list of who reacted built on it.
useMessageDateSeparator
Returns the date to show in a separator above a message.
useMessageDateSeparator(params: {
hideDateSeparators?: boolean;
message?: LocalMessage;
previousMessage?: LocalMessage;
}): Date | undefined- Returns: the message's
created_atas aDatewhen it falls on another day thanpreviousMessage, or when there's no previous message.undefinedotherwise, and always withhideDateSeparators. - Reads: its params only.
- Re-renders: never by itself.
- Needs: nothing.
useMessageGroupStyles
Returns where a message sits in its group of consecutive messages from one user.
useMessageGroupStyles(params: {
message: LocalMessage;
previousMessage?: LocalMessage;
nextMessage?: LocalMessage;
dateSeparatorDate?: Date;
maxTimeBetweenGroupedMessages?: number;
noGroupByUser?: boolean;
getMessageGroupStyle: MessagesContextValue["getMessageGroupStyle"];
}): string[]- Returns: the group styles from
getMessageGroupStyle, such as['top'],['middle'],['bottom']or['single']. Without agetMessageGroupStyle, the SDK's own function.[]withnoGroupByUser. - Reads: its params only.
- Re-renders: never by itself.
- Needs: nothing.
The message lists use these two hooks for each row. They render InlineDateSeparator with the date, and pass the group styles to Message. Use them in a message list of your own.
useTranslatedMessage
Returns a message with its text in the user's language.
useTranslatedMessage(message?: LocalMessage | MessageResponse): LocalMessage | MessageResponse | undefined- Returns: a copy of the message whose
textismessage.i18n.<userLanguage>_textwhen the message has that translation, and the message's own text otherwise. A deleted message keeps its text.undefinedwithout a message. - Reads:
userLanguagefromTranslationContext. - Re-renders: when the user's language changes.
- Needs:
ChatorOverlayProvider. Throws outside both.
getTranslatedMessage(message, userLanguage) does the same outside a component, with the language you pass. The SDK's message text, channel preview and pinned messages show the text the same way. See Message translation.
Best practices
- Inside a message, take the handlers from
useMessageContext()oruseMessageActionHandlers(). CalluseMessageOperations()only for an operation the handlers don't cover, or outside a message. - Call
useMessageActions()in a custom menu, not in every row. - Check what the user may do with
useOwnCapabilitiesContext()before you show an action, so a user never sees a button the server would refuse. - Wrap
useMessageOperations()calls intry/catch, and report the outcome withuseNotificationApi. The handlers that publish a notification already do. - Edit a message in the composer with
setEditingState(message)fromuseMessageComposerAPIContext(), orhandleEditMessage(). There's no edit operation.
Examples
Mark unread
The handler publishes its own notification, success or failure:
import { Pressable, Text } from "react-native";
import {
useMessageActionHandlers,
useMessageContext,
useOwnCapabilitiesContext,
} from "stream-chat-react-native";
export const MarkUnreadButton = () => {
const { isMyMessage } = useMessageContext();
const { handleMarkUnreadMessage } = useMessageActionHandlers();
const { readEvents } = useOwnCapabilitiesContext();
if (isMyMessage || !readEvents) return null;
return (
<Pressable
accessibilityRole="button"
onPress={() => void handleMarkUnreadMessage()}
>
<Text>Mark unread</Text>
</Pressable>
);
};Reaction buttons
import { Pressable, Text, View } from "react-native";
import { useMessageContext } from "stream-chat-react-native";
export const QuickReactions = () => {
const { handleReaction } = useMessageContext();
if (!handleReaction) return null;
return (
<View style={{ flexDirection: "row" }}>
<Pressable
accessibilityRole="button"
onPress={() => void handleReaction("love")}
>
<Text>Love</Text>
</Pressable>
<Pressable
accessibilityRole="button"
onPress={() => void handleReaction("like")}
>
<Text>Like</Text>
</Pressable>
</View>
);
};Use the types of your supportedReactions. The defaults are like, haha, love, wow and sad, and the emoji types of the full picker: see Reactions.
Retry a failed message
import { Pressable, Text } from "react-native";
import type { LocalMessage } from "stream-chat";
import { useMessageOperations } from "stream-chat-react-native";
export const RetryButton = ({ message }: { message: LocalMessage }) => {
const { retrySendMessage } = useMessageOperations();
if (message.status !== "failed") return null;
return (
<Pressable
accessibilityRole="button"
onPress={() => retrySendMessage(message).catch(() => undefined)}
>
<Text>Retry</Text>
</Pressable>
);
};A failed retry leaves the message marked failed, so the button stays. The Custom message UI cookbook builds a whole message component on these hooks.