renderText function

renderText controls how message text is formatted. By default it parses Markdown and returns a ReactElement using ReactMarkdown from react-markdown, with the remark parser and remark/rehype plugins.

The default remark plugins used by SDK are:

  1. remark-gfm - a third party plugin to add GitHub-like markdown support

The default rehype plugins (both specific to this SDK) are:

  1. plugin to render user mentions
  2. plugin to render emojis

Best Practices

  • Keep Markdown enabled unless your product requires strict plain text.
  • Sanitize or restrict allowed tags when rendering untrusted content.
  • Add custom plugins sparingly to avoid performance regressions.
  • Keep mention and emoji rendering consistent across message views.
  • Test custom renderers with long messages and mixed formatting.

Overriding Defaults

Custom renderText Function

If you don’t want Markdown support, pass a custom renderText function that returns a React node. You can provide it to MessageList or MessageSimple via the renderText prop.

For example, here’s a minimal version that wraps text in a <span>:

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

const customRenderText = (text) => {
  return <span>{text}</span>;
};

export const WrappedMessageList = () => (
  <MessageList renderText={customRenderText} />
);

Here’s an example with VirtualizedMessageList, which doesn’t accept renderText directly:

import { VirtualizedMessageList, MessageSimple } from "stream-chat-react";

const customRenderText = (text) => {
  return <span>{text}</span>;
};

const CustomMessage = (props) => (
  <MessageSimple {...props} renderText={customRenderText} />
);

export const WrappedVirtualizedMessageList = () => (
  <VirtualizedMessageList Message={CustomMessage} />
);

Custom Element Rendering

If you like the default output but want to customize how specific ReactMarkdown components render (for example, **strong**), pass options as the third argument to renderText:

mention and emoji are special component types generated by the SDK’s custom rehype plugins.

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

const CustomStrongComponent = ({ children }) => (
  <b className="custom-strong-class-name">{children}</b>
);

const CustomMentionComponent = ({ children, node: { mentionedUser } }) => (
  <a data-user-id={mentionedUser.id} href={`/user-profile/${mentionedUser.id}`}>
    {children}
  </a>
);

export const WrappedMessageList = () => (
  <MessageList
    renderText={(text, mentionedUsers) =>
      renderText(text, mentionedUsers, {
        customMarkDownRenderers: {
          strong: CustomStrongComponent,
          mention: CustomMentionComponent,
        },
      })
    }
  />
);

Custom Remark and Rehype Plugins

To extend the plugin list, provide custom remark/rehype arrays via getRemarkPlugins and getRehypePlugins. These receive the default arrays, and you return the final list. Pass them as the third renderText parameter.

To understand remark/rehype plugins, start with react-remark, which is used under the hood.

import { renderText, RenderTextPluginConfigurator } from "stream-chat-react";
import { customRehypePlugin } from "./rehypePlugins";
import { customRemarkPlugin } from "./remarkPlugins";

const getRehypePlugins: RenderTextPluginConfigurator = (plugins) => {
  return [customRehypePlugin, ...plugins];
};
const getRemarkPlugins: RenderTextPluginConfigurator = (plugins) => {
  return [customRemarkPlugin, ...plugins];
};

const customRenderText = (text, mentionedUsers) =>
  renderText(text, mentionedUsers, {
    getRehypePlugins,
    getRemarkPlugins,
  });

const WrappedMessageList = () => <MessageList renderText={customRenderText} />;

You can also define a custom set of allowed tag names. For tree transforms, use libraries like unist-builder plus unist-util-visit or hast-util-find-and-replace:

import { findAndReplace } from "hast-util-find-and-replace";
import { u } from "unist-builder";
import {
  defaultAllowedTagNames,
  renderText,
  RenderTextPluginConfigurator,
} from "stream-chat-react";

// wraps every letter b in <xxx></xxx> tags
const customTagName = "xxx";
const replace = (match) =>
  u("element", { tagName: customTagName }, [u("text", match)]);
const customRehypePlugin = () => (tree) => findAndReplace(tree, /b/, replace);

const getRehypePlugins: RenderTextPluginConfigurator = (plugins) => {
  return [customRehypePlugin, ...plugins];
};

const customRenderText = (text, mentionedUsers) =>
  renderText(text, mentionedUsers, {
    allowedTagNames: [...defaultAllowedTagNames, customTagName],
    getRehypePlugins,
  });

const WrappedMessageList = () => <MessageList renderText={customRenderText} />;

Existing Remark and Rehype Plugins

The SDK already exports the following plugins:

Remark plugins

  • htmlToTextPlugin: Treats HTML nodes found in the Markdown as plain text (no HTML is parsed or executed). Useful for preventing inline HTML from rendering.
  • imageToLink: Converts Markdown images (![alt](url)) into links (<a href="url">label</a>). The link text defaults to the image URL, but can be configured to use the image alt or title.
  • keepLineBreaksPlugin: Preserves multiple blank source lines by inserting <br> elements between block nodes so visual spacing matches the original input.
  • plusPlusToEmphasis: Adds support for “inserted” text using ++text++, which renders as <ins>text</ins>. Skips code, math, and links.
  • remarkIgnoreMarkdown: Disables Markdown parsing entirely and renders the original input as plain text inside a single paragraph.

Rehype plugins

  • mentionsMarkdownPlugin(mentionedUsers): Detects @username/@id mentions (based on the provided mentionedUsers) and wraps them in a custom <mention> element with a mentionedUser payload. Handles e‑mail-like usernames that GFM splits into mailto: links.
  • emojiMarkdownPlugin: Wraps native Unicode emoji in a custom <emoji> element, enabling consistent styling/behavior for emoji in rendered output.

mentionsMarkdownPlugin is a factory function that needs to receive an array of UserResponse objects (mentionedUsers) when it is pushed to the rehype plugins array returned by getRehypePlugins.

To select which plugins to apply:

import {
  keepLineBreaksPlugin,
  MessageList,
  renderText,
  useChannelStateContext,
} from "stream-chat-react";

const customRenderText = (text?: string, mentionedUsers?: UserResponse[]) =>
  renderText(text, mentionedUsers, {
    getRemarkPlugins: () => [keepLineBreaksPlugin],
    // disabled all the SDK's rehype plugins
    getRehypePlugins: () => [],
  });

const WrappedMessageList = () => <MessageList renderText={customRenderText} />;