Skip to content

Markdown Rendering

AI responses almost always contain Markdown — headings, lists, code blocks, bold text, etc. Since useChat gives you raw text in parts, you bring your own Markdown renderer.

Terminal window
pnpm add react-markdown

Render text parts with react-markdown instead of plain <span>:

import { useChat } from "@devscalelabs/react-sse-chat";
import ReactMarkdown from "react-markdown";
function Chat() {
const { messages, isLoading, sendMessage, stop } = useChat({
api: "/chat",
});
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => {
switch (part.type) {
case "text":
return <ReactMarkdown key={i}>{part.text}</ReactMarkdown>;
default:
return null;
}
})}
</div>
))}
{isLoading && <button onClick={stop}>Stop</button>}
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem("message") as HTMLInputElement;
sendMessage(input.value);
input.value = "";
}}
>
<input name="message" placeholder="Type a message..." />
<button type="submit" disabled={isLoading}>Send</button>
</form>
</div>
);
}

Add syntax highlighting with react-syntax-highlighter:

Terminal window
pnpm add react-syntax-highlighter
pnpm add -D @types/react-syntax-highlighter
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
function MarkdownContent({ content }: { content: string }) {
return (
<ReactMarkdown
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
const isInline = !match;
return isInline ? (
<code className={className} {...props}>
{children}
</code>
) : (
<SyntaxHighlighter
style={oneDark}
language={match[1]}
PreTag="div"
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
);
},
}}
>
{content}
</ReactMarkdown>
);
}

Then use it in your message rendering:

{msg.parts.map((part, i) => {
switch (part.type) {
case "text":
return <MarkdownContent key={i} content={part.text} />;
default:
return null;
}
})}

Typically, user messages are plain text while assistant messages are Markdown. You can render them differently:

{messages.map((msg) => (
<div key={msg.id}>
{msg.parts.map((part, i) => {
if (part.type !== "text") return null;
return msg.role === "assistant" ? (
<ReactMarkdown key={i}>{part.text}</ReactMarkdown>
) : (
<span key={i}>{part.text}</span>
);
})}
</div>
))}

react-markdown is the most common choice, but you can use any Markdown renderer:

LibraryNotes
react-markdownMost popular. Supports remark/rehype plugins.
marked + dangerouslySetInnerHTMLFaster, but requires sanitization.
markdown-itPlugin-rich, similar tradeoffs to marked.
MDXIf you need JSX inside Markdown.

The parts model works with any of these — you always have the raw text string to pass to your renderer.