This is beta documentation for Stream Chat React SDK v14. For the latest stable version, see the latest version (v13) .

Reactions Customization

This example shows how to customize the SDK reaction surfaces for a channel subtree.

Best Practices

  • Keep custom reactions aligned with your product’s interaction model.
  • Register shared reaction options through WithComponents so the selector and list stay in sync.
  • Prefer the { quick, extended } shape when you need different sets for the compact selector and expanded picker.
  • Keep custom reaction handlers permission-aware and idempotent.
  • Test custom reactions in both channel and thread message lists.

Shared Reaction Options

reactionOptions lives in ComponentContext, so the usual override path is WithComponents.

import { Channel, WithComponents } from "stream-chat-react";

const customReactionOptions = {
  quick: {
    arrow_down: {
      Component: () => <>⬇️</>,
      name: "Down vote",
    },
    arrow_up: {
      Component: () => <>⬆️</>,
      name: "Up vote",
    },
  },
  extended: {
    arrow_down: {
      Component: () => <>⬇️</>,
      name: "Down vote",
    },
    arrow_up: {
      Component: () => <>⬆️</>,
      name: "Up vote",
    },
    fire: {
      Component: () => <>🔥</>,
      name: "Fire",
    },
  },
};

export const WrappedChannel = ({ children }) => (
  <WithComponents overrides={{ reactionOptions: customReactionOptions }}>
    <Channel>{children}</Channel>
  </WithComponents>
);

ReactionsList still supports the older array form when you pass reactionOptions directly to that component, but ReactionSelector now uses the shared context value and the { quick, extended } shape is the best fit for selector customization.

Override The Default Reaction Surfaces

Use WithComponents to replace ReactionSelector, ReactionsList, or MessageReactionsDetail.

import {
  Channel,
  MessageReactionsDetail,
  ReactionSelector,
  ReactionsList,
  WithComponents,
} from "stream-chat-react";

const CustomReactionSelector = (props) => <ReactionSelector {...props} />;

const CustomReactionsList = (props) => (
  <ReactionsList {...props} visualStyle="segmented" />
);

const CustomMessageReactionsDetail = (props) => (
  <MessageReactionsDetail {...props} />
);

export const WrappedChannel = ({ children }) => (
  <WithComponents
    overrides={{
      MessageReactionsDetail: CustomMessageReactionsDetail,
      ReactionSelector: CustomReactionSelector,
      ReactionsList: CustomReactionsList,
    }}
  >
    <Channel>{children}</Channel>
  </WithComponents>
);

Here's the difference between the two visualStyle options for ReactionsList:

ClusteredSegmented

Custom Reaction Handler

If you want to change behavior without replacing the whole selector UI, wrap ReactionSelector and override handleReaction.

import { useCallback } from "react";
import {
  Channel,
  ReactionSelector,
  WithComponents,
  useChannelStateContext,
  useMessageContext,
} from "stream-chat-react";

const CustomReactionSelector = (props) => {
  const {
    message: { id: messageId, own_reactions: ownReactions = [] },
  } = useMessageContext("CustomReactionSelector");
  const { channel } = useChannelStateContext("CustomReactionSelector");

  const handleReaction = useCallback(
    async (reactionType, event) => {
      console.log({ event });

      const hasReactedWithType = ownReactions.some(
        (reaction) => reaction.type === reactionType,
      );

      if (hasReactedWithType) {
        await channel.deleteReaction(messageId, reactionType);
        return;
      }

      await channel.sendReaction(messageId, { type: reactionType });
    },
    [channel, messageId, ownReactions],
  );

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

export const WrappedChannel = ({ children }) => (
  <WithComponents overrides={{ ReactionSelector: CustomReactionSelector }}>
    <Channel>{children}</Channel>
  </WithComponents>
);

Read More

The reference page for the current reaction surfaces lives here: