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

Reactions

What users get

Users react to a message with an emoji, and see who reacted with what. A reaction shows on the message at once, for everyone viewing it.

  • Adding a reaction. The smiley button next to a message, or Add reaction in its menu, opens the reaction selector with the quick reactions. When extended reactions are configured, a + button opens the full list. Picking a reaction the user already gave removes it.
  • Reactions on the message. The reactions sit at the edge of the message bubble with their counts. By default they're clustered into one pill; the segmented style shows one chip per reaction type.
  • Who reacted. Clicking the reactions opens a dialog that lists the users who reacted, filtered by reaction type or showing all of them. The dialog has its own button to add a reaction.
Clustered Segmented
Clustered reactions
Segmented reactions

How it works across the layers

State

A message carries its reactions in three fields:

  • reaction_groups: one summary per reaction type, with the count and when it was first and last used;
  • latest_reactions: the most recent reactions, with their users;
  • own_reactions: the current user's reactions.

The state layer applies reaction.new, reaction.updated and reaction.deleted events to the message, wherever it's shown: the channel's list, a thread, or a thread's parent message.

The message operations of a channel or a thread change reactions optimistically:

  • MessageOperations.addReaction({ messageId, reaction, options }) adds the reaction to the message, then sends the request. With options: { enforce_unique: true }, the new reaction replaces the user's other reactions on the message.
  • MessageOperations.deleteReaction({ messageId, type }) removes the user's reaction of that type, then sends the request.

When the server answers, its copy of the message replaces the local one. If the request fails, the change is undone, unless the request was queued to be sent later. See Message operations.

UI

The user picks 👍 on Ben's message:

  1. ReactionSelector calls handleReaction("like") from the message context.
  2. The handler checks own_reactions. The user hasn't reacted with like yet, so it adds the reaction; had they, it would remove it.
  3. The message updates in the list at once, and the handler sends the request with the reaction's emoji as emoji_code, so push notifications can show it. If the request fails, the message goes back to how it was. The default handler calls client.sendReaction() and client.deleteReaction() itself, not messageOperations, so a reaction made while offline isn't queued.
  4. MessageReactions re-renders from the message's reaction_groups and own_reactions, and marks the user's reaction as theirs.
Part Component
The quick reactions ReactionSelector
The full list behind + ReactionSelectorExtendedList
The reactions on a message MessageReactions
The dialog that lists who reacted MessageReactionsDetail
The reactions that are offered, and their emoji reactionOptions in the component context

The dialog loads the users with client.queryReactions(), 25 at a time, up to 1000 reactions. For a message with more than 1000 reactions, the default MessageReactions doesn't open the dialog.

Turning it on or off

Reactions are always available. The send-reaction capability decides who can add them: without it, the message actions leave out the smiley button and Add reaction. The server derives the capability from the user's role and the channel type's settings, which include reactions.

To hide reactions in your UI, remove the react actions from the message action set (see Actions), and replace MessageReactions with a component that renders nothing.

Customizing

The reactions on offer

reactionOptions in the component context lists the reactions. Set it with WithComponents, so the selector, the reactions on a message and the dialog all use the same set. quick is the row in the selector; extended is the full list behind +:

import type { ReactionOptions } from "stream-chat-react";
import { emojiToUnicode, WithComponents } from "stream-chat-react";

const reactionOptions: ReactionOptions = {
  quick: {
    arrow_up: {
      Component: () => <>⬆️</>,
      name: "Up vote",
      unicode: emojiToUnicode("⬆️"),
    },
    arrow_down: {
      Component: () => <>⬇️</>,
      name: "Down vote",
      unicode: emojiToUnicode("⬇️"),
    },
  },
  extended: {
    fire: {
      Component: () => <>🔥</>,
      name: "Fire",
      unicode: emojiToUnicode("🔥"),
    },
    rocket: {
      Component: () => <>🚀</>,
      name: "Rocket",
      unicode: emojiToUnicode("🚀"),
    },
  },
};

export const App = () => (
  <WithComponents overrides={{ reactionOptions }}>
    {/* the rest of the app */}
  </WithComponents>
);

Each key is the reaction type stored on the server. The handler turns unicode back into the emoji and sends it as emoji_code; leave it out and push notifications have no emoji to show. To offer the whole emoji set as extended reactions, mapEmojiMartData(data) turns the @emoji-mart/data set into an extended map.

Messages with up-vote and down-vote reactions from a custom reaction set

Style and position

MessageReactions takes visualStyle ("clustered" or "segmented") and verticalPosition ("top" or "bottom"). Replace it with a version that sets them:

import type { MessageReactionsProps } from "stream-chat-react";
import { MessageReactions, WithComponents } from "stream-chat-react";

const SegmentedReactions = (props: MessageReactionsProps) => (
  <MessageReactions
    {...props}
    verticalPosition="bottom"
    visualStyle="segmented"
  />
);

export const App = () => (
  <WithComponents overrides={{ MessageReactions: SegmentedReactions }}>
    {/* the rest of the app */}
  </WithComponents>
);

Segmented reactions at the top show at most four chips and a counter for the rest. At the bottom, every reaction type shows.

Order

By default, reaction types are in the order they were first used on the message. Pass sortReactions to MessageList or VirtualizedMessageList to change it:

import type { MessageListProps } from "stream-chat-react";
import { MessageList } from "stream-chat-react";

const byCount: MessageListProps["sortReactions"] = (a, b) =>
  b.reactionCount - a.reactionCount;

export const SortedMessageList = () => <MessageList sortReactions={byCount} />;

reactionDetailsSort on the same lists sets the order of users in the dialog. It's sent to the server with the query, and defaults to the newest first.

Changing what a reaction does

ReactionSelector takes a handleReaction prop that replaces the message's handler. This selector allows one reaction per user, through the state layer's messageOperations. The ReactionSelector override key is typed as a forwardRef component, so wrap the replacement in forwardRef:

import { forwardRef } from "react";
import type { ReactionSelectorProps } from "stream-chat-react";
import {
  ReactionSelector,
  useChannel,
  useMessageContext,
  useThreadContext,
  WithComponents,
} from "stream-chat-react";

// One reaction per user: a new reaction replaces the previous one.
const SingleReactionSelector = forwardRef<
  HTMLDivElement,
  ReactionSelectorProps
>(function SingleReactionSelector(props, _ref) {
  const { message } = useMessageContext();
  const channel = useChannel();
  const thread = useThreadContext();
  const { messageOperations } = thread ?? channel;

  const handleReaction = async (type: string) => {
    const alreadyReacted = message.own_reactions?.some(
      (reaction) => reaction.type === type,
    );
    if (alreadyReacted) {
      await messageOperations.deleteReaction({ messageId: message.id, type });
      return;
    }
    await messageOperations.addReaction({
      messageId: message.id,
      options: { enforce_unique: true },
      reaction: { type },
    });
  };

  return <ReactionSelector {...props} handleReaction={handleReaction} />;
});

export const App = () => (
  <WithComponents overrides={{ ReactionSelector: SingleReactionSelector }}>
    {/* the rest of the app */}
  </WithComponents>
);

The handler covers the selector and its extended list. The add step in the reactions dialog still uses the message's handler, unless you also replace MessageReactionsDetail.

The reactions dialog

MessageReactionsDetail is an override key. If you rebuild it, keep the default behaviour:

  • selectedReactionType={null} means "show all reactions". Clicking the selected type again goes back to it. With no type selected, each user's row also shows the emoji they picked.
  • The clustered MessageReactions opens the dialog showing all reactions; a segmented chip opens it filtered to its type.
  • The add button switches the dialog to ReactionSelectorExtendedList.
  • MessageReactionsDetailLoadingIndicator is the default loading skeleton, ready to reuse.

ReactionSelector, ReactionSelectorExtendedList and MessageReactions are override keys too. See Overriding components.

Reference