Closed KevinWang15 closed 3 months ago
If we could just let the user specify a <Code>
component for code
instead of having to write a function, and update the props of <Code>
instead of remounting it, this problem could be solved.
I checked many LLM chatbot frontends, they all have this problem. If this could get fixed then it will improve the quality of life for many developers!
hmmm I tried again, is it as simple as this?
<ReactMarkdown
components={{
code: SyntaxHighlighter
}}
>
{markdown}
</ReactMarkdown>
If so then it is already possible?.. Let me confirm..
Sure. But it has nothing to do with this project. Just with react.
You pass new components (new functions) each time. Don‘t.
Hi! This was closed. Team: If this was fixed, please add phase/solved
. Otherwise, please add one of the no/*
labels.
Thanks a lot @wooorm , it works..
import React, {useEffect, useState} from 'react';
import ReactMarkdown from 'react-markdown';
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter';
import {darcula} from 'react-syntax-highlighter/dist/esm/styles/prism';
const Code = ({className, children, ...props}) => {
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter
language={match[1]}
{...props}
style={darcula}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
};
const StreamingCodeBlock = () => {
const [markdown, setMarkdown] = useState('```javascript\n// Initial code\n');
useEffect(() => {
const interval = setInterval(() => {
setMarkdown(prev => prev + `console.log('New line ${Date.now()}');\n`);
}, 500);
return () => clearInterval(interval);
}, []);
return (
<ReactMarkdown components={{code: Code}}>
{markdown}
</ReactMarkdown>
);
};
But again I see a lot of Chatbot UIs doing it wrong, ~like lobe-chat (https://github.com/lobehub/lobe-ui/blob/5a94c8c61443c01400711dc2c956649f1db68765/src/Markdown/index.tsx#L71) cc maintainer of lobe chat here @canisminor1990.~ (update: lobe-chat correctly added useMemo
so I think it should produce the correct result, but I just tested lobe-chat and it has the same issue as the one in the above video)
Maybe we could put a small hint in the documentation 😁. Using
components={{
code: SyntaxHighlighter // SyntaxHighlighter is a react component
}}
is more efficient and more correct in many cases than a callback function (which was in the docs).
I dunno, I feel like developers need to know some react themselves, how to memo-ize things, speed components up, be efficient.
The examples also work with document.body
and with literal strings of markdown, which you probably don’t want.
I believe the issue is with the code property in your components. On each render, a new function is created and assigned to code, causing React to think the component has changed and trigger a re-render. I've tested that wrapping this function with useCallback solves the problem:
const CodeComponent = useCallback(({ className, children, inline }) => {
if (inline) {
return <code>{children}</code>;
}
const match = /language-(\w+)/.exec(className || '');
const language = (match && match[1]) || '';
return (
<SyntaxHighlighter language={language}>
{String(children)}
</SyntaxHighlighter>
);
}, []); // Empty dependency array
<ReactMarkdown components={{ code: CodeComponent }}>
{markdown}
</ReactMarkdown>
This ensures the code property always references the same function, preventing unnecessary re-renders.
Yes, define the component outside of the component that renders <ReactMarkdown />
Initial checklist
Problem
When using ReactMarkdown with custom components (particularly for code blocks), any change to the markdown content causes the component to remount. This leads to issues such as loss of user selections and interactions within these components.
For example
If you try to select some code, each time the markdown is updated, the
<SyntaxHighlighter>
gets remounted, and user selection is lost.https://github.com/user-attachments/assets/d216dc0d-cd30-4ef3-8217-e71a1cdf9c1b
The use case is with LLM chatbots.
Markdown content is often generated and displayed in a streaming fashion with LLM chatbots.
The user sees the beginning of the code and starts interacting with it (e.g., selecting parts of the code, placing the cursor for copying).
But with the current implementation, as the AI continues to generate more code, appending to the existing block, the ReactMarkdown component is remounted and the user selection is lost, leading to a frustrating experience for users who are trying to interact with the code while it's still being generated.
Solution
I don't have a solution - I'm not familiar with the internals of ReactMarkdown.
But I think this should be possible.
Alternatives
I tried but couldn't find any workaround. Updating code in ReactMarkdown to make it possible is the only solution.