Reactions Customization

In this example, we’ll override the SDK’s default reaction set (defaultReactionOptions) with up/down arrows to simulate voting.

Best Practices

  • Keep custom reactions aligned with your product’s interaction model.
  • Ensure all supported reaction types are included to avoid degraded UI.
  • Provide a full set for ReactionsList even if the selector shows a subset.
  • Prefer custom handlers when changing behavior without replacing UI.
  • Test reactions with permissions and moderation policies enabled.

Under the hood, ReactionSelector, ReactionsList, SimpleReactionsList, and ReactionsListModal render emoji components from reactionOptions. Your custom array must match ReactionOptions:

type ReactionOptions = Array<{
  type: string;
  Component: React.ComponentType;
  name?: string;
}>;

Let’s build a simple list with 'arrow_up' and 'arrow_down' emojis. To override the defaults, pass the list via Channel so default components can read it:

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

const customReactionOptions = [
  {
    type: "arrow_up",
    Component: () => <>⬆️</>,
    name: "Upwards Black Arrow",
  },
  {
    type: "arrow_down",
    Component: () => <>⬇️</>,
    name: "Downwards Black Arrow",
  },
];

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

If a reaction type is missing from the list, it won’t be registered and can lead to a degraded experience.

You can also pass options directly to the default components (component props override the Channel value):

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

const CustomReactionsList = (props) => (
  <ReactionsList {...props} reactionOptions={customReactionOptions} />
);

// ReactionSelector component requires forwarded reference
const CustomReactionSelector = forwardRef((props, ref) => (
  <ReactionSelector
    {...props}
    ref={ref}
    reactionOptions={selectorReactionOptions}
  />
));

export const WrappedChannel = ({ children }) => (
  <Channel
    ReactionsList={CustomReactionsList}
    ReactionSelector={CustomReactionSelector}
  >
    {children}
  </Channel>
);

ReactionSelector can show a subset of reactions, but ReactionsList should generally have the full set.

Custom reactionOptions rendered through ReactionSelector

Custom reactionOptions rendered through ReactionsList

Custom Reaction Handler

To adjust behavior without replacing the UI, provide a custom handleReaction:

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

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

  const handleReaction = useCallback(
    async (reactionType, event) => {
      // your custom logic with default behavior (minus optimistic updates)

      console.log({ event });

      const hasReactedWithType =
        (ownReactions ?? []).some(
          (reaction) => reaction.type === reactionType,
        ) ?? false;

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

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

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

// and then just add it to ComponentContext
export const WrappedChannel = ({ children }) => (
  <Channel ReactionSelector={CustomReactionSelector}>{children}</Channel>
);

Read More

See Introducing new reactions in the v11 upgrade guide for more options.