import { MessageList } from "stream-chat-react";
const customRenderText = (text) => {
return <span>{text}</span>;
};
export const WrappedMessageList = () => (
<MessageList renderText={customRenderText} />
);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:
remark-gfm- a third party plugin to add GitHub-like markdown support
The default rehype plugins (both specific to this SDK) are:
- plugin to render user mentions
- 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>:
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 () into links (<a href="url">label</a>). The link text defaults to the image URL, but can be configured to use the imagealtortitle.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/@idmentions (based on the providedmentionedUsers) and wraps them in a custom<mention>element with amentionedUserpayload. Handles e‑mail-like usernames that GFM splits intomailto: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} />;