Markdown Rendering in React
One of our project requirements was to support Markdown rendering, aiming to replicate effects similar to ChatGPT and Claude.
This article aims to document the problems I encountered and their solutions while implementing this feature.
This is an English translation of an article originally published in Chinese on February 20, 2024. The content of the original article may be outdated or deprecated. Please verify the current status of any tools, libraries, or methods mentioned before implementing them in your projects.
react-markdown
First, install react-markdown
.
1 | npm install react-markdown |
Let's test using the official example text from the react-markdown
library:
1 | # A demo of `react-markdown` |
In the test text, there are also code blocks, but since I'm using Hexo for my blog, these texts that should be in code blocks (as I set) were all rendered as code blocks.
Therefore, I added a backslash after each code block to prevent them from being rendered as code blocks. You can remove these backslashes when testing.
1 | import ReactMarkdown from "react-markdown"; |
The rendered effect (remember to replace the value of markdownContent
):
As you can see, there are some differences compared to Typora or GitHub's rendering effects. For example, the code blocks and regular text styles are too similar, and we'd prefer code blocks with distinct background colors and syntax highlighting.
Additionally, special styles like tables, task lists, and strikethrough weren't rendered.
remark-gfm
Actually, the test text already told us why: these are GFM (GitHub Flavored Markdown) features, and react-markdown
doesn't support GFM by default.
So we need to install the remark-gfm
plugin to support GFM:
1 | npm install remark-gfm |
Using plugins with react-markdown
is quite simple:
1 | import remarkGfm from "remark-gfm"; |
The rendered effect:
At this point, you're probably wondering: where are the table borders??
If we inspect the table's styles using developer tools, we'll find that the table is indeed rendered as a table
tag, but the User Agent Stylesheet has applied some processing to the table
tag. So we need to write some styles to override these default styles:
1 | table { |
Add !important
to all styles to override the User Agent Stylesheet styles.
The rendered effect now:
Due to security considerations, I decided not to use HTML support, so I won't demonstrate it here.
Now we've completed a simple Markdown rendering feature. However, there's one crucial feature in this requirement: code block highlighting.
Code Block Highlighting
Due to project requirements, code blocks must be prominent and have syntax highlighting, which applies to inline code blocks as well.
Here we can use react-syntax-highlighter
.
1 | npm install react-syntax-highlighter |
react-syntax-highlighter
has two engines: prism
and highlight.js
. You can search online for their detailed differences.
Here we'll use the prism
engine with the oneDark
theme:
1 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; |
In the component
prop of ReactMarkdown
, we can customize how code blocks are rendered. If you've seen other tutorials online, they almost all write it like this:
1 | <ReactMarkdown |
Here we customized the rendering behavior of the code
component.
The parameters in
code({ node, inline, className, children, ...props }) {}
represent:
node
: current nodeinline
: whether it's an inline code blockclassName
: class namechildren
: child nodes (content in code block)...props
: other properties
Then we use regex to match language-xxx
in className
, and if there's a match and it's not an inline code block, we use SyntaxHighlighter
to render the code block; otherwise, we use the default code
tag.
But!!!
The inline
property has been deprecated and won't be passed as a parameter in the new version of react-markdown
!!!
This really gave me a headache at the time, and I couldn't find a solution after searching online. I decided to temporarily solve another problem: rendering code blocks without defined languages.
After rendering, the biggest difference between code blocks and inline code blocks is that code blocks have a pre
tag wrapper. Since the code
tag as a child tag can't get the parent tag's styles from props
, but thinking reversely, the pre
tag can get the code
tag's styles!
So I wrote this wild solution:
1 | <Markdown |
Directly check the children
's type in the pre
tag - if it's code
, then it must be a code block, and then match the language type in className
.
If there's no match, it will cause an error, so I temporarily added a try...catch
statement - if there's no match, just default to rendering it as a Python language code block.
The effect is not bad:
Afterword
There are still some issues with this implementation, such as unresolved inline code block rendering, the try...catch
statement not being a good solution, and the technical debt of defaulting to Python language for undefined language code blocks is quite messy!
However, due to project time constraints, I didn't delve deeper into these issues. Perhaps when I have more time in the future, I'll revisit and solve these problems. That's for another time.