Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to prevent remounting of components (<ReactMarkdown components={...}>) when markdown changes? #849

Closed
4 tasks done
KevinWang15 opened this issue Jul 31, 2024 · 6 comments
Labels
🙋 no/question This does not need any changes 👎 phase/no Post cannot or will not be acted on

Comments

@KevinWang15
Copy link

KevinWang15 commented Jul 31, 2024

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

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 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({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>
                    );
                },
            }}
        >
            {markdown}
        </ReactMarkdown>
    );
};

If you try to select some code, each time the markdown is updated, the <SyntaxHighlighter> gets remounted, and user selection is lost.

CleanShot.2024-07-31.at.19.07.39.mp4

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.

@github-actions github-actions bot added 👋 phase/new Post is being triaged automatically 🤞 phase/open Post is being triaged manually and removed 👋 phase/new Post is being triaged automatically labels Jul 31, 2024
@KevinWang15
Copy link
Author

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!

@KevinWang15
Copy link
Author

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..

@wooorm
Copy link
Member

wooorm commented Jul 31, 2024

Sure. But it has nothing to do with this project. Just with react.

You pass new components (new functions) each time. Don‘t.

@wooorm wooorm closed this as not planned Won't fix, can't repro, duplicate, stale Jul 31, 2024

This comment has been minimized.

@wooorm wooorm added the 🙋 no/question This does not need any changes label Jul 31, 2024
@github-actions github-actions bot added 👎 phase/no Post cannot or will not be acted on and removed 🤞 phase/open Post is being triaged manually labels Jul 31, 2024
@KevinWang15
Copy link
Author

KevinWang15 commented Jul 31, 2024

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).

@wooorm
Copy link
Member

wooorm commented Aug 1, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🙋 no/question This does not need any changes 👎 phase/no Post cannot or will not be acted on
Development

No branches or pull requests

2 participants