Customizing Markdown Rendering with react-markdown and useRef
This article demonstrates how to use react-markdown for personalized Markdown rendering, leveraging useRef to handle dynamic data updates and prevent unnecessary re‑renders, while providing full code examples and alternative implementation strategies.
Introduction to react-markdown
Markdown is a crucial tool in AIGC workflows for document generation and content display. Rendering Markdown on a page is a primary interaction method for AIGC products, and react-markdown converts Markdown text into React components, making it easy to display formatted content.
<code>import React from 'react';
import { createRoot } from 'react-dom/client';
import Markdown from 'react-markdown';
const markdown = '# Hi, *Pluto*!';
createRoot(document.body).render(<Markdown>{markdown}</Markdown>);
</code>Personalized Markdown Rendering
While react-markdown handles standard rendering, custom requirements—such as turning a normal link into a citation superscript that opens a drawer with source details—require additional development. react-markdown supports custom component rendering via the components prop.
<code><Markdown components={{
h1: 'h2',
// Rewrite <em> to <i> with red color
em: (props) => {
const { node, ...rest } = props;
return <i style={{ color: 'red' }} {...rest} />;
}
}} />
</code>The backend supplies data in the following shape:
<code>{
content: 'Markdown 是一种... [1](citation:abc)',
citations: {
'abc': {
detail: '本段文字参考了...',
goto: 'a.b.c/example'
}
}
}
</code>The
contentfield contains the Markdown to render, while the citation reference
[1](citation:abc)points to
citations.abc. Because the citation data is fetched incrementally, the component must poll the backend for updates.
Code Implementation
The component hierarchy consists of:
App – the main application that fetches data and passes it to the Markdown component.
Markdown – the react-markdown component that renders the Markdown text.
Link – a custom component that renders tags as citation superscripts and opens a drawer.
Citation – the superscript component that triggers the drawer.
Drawer – a modal that displays citation details.
Link component implementation:
<code>const Link = (props) => {
const { detail, goto, children } = props;
const [showDrawer, setShowDrawer] = useState(false);
return (
<>
<Citation onClick={() => setShowDrawer(true)}>{children}</Citation>
{showDrawer ? (
<Drawer detail={detail} goto={goto} onClose={() => setShowDrawer(false)} />
) : null}
</>
);
};
</code>Function that creates custom components based on citation data:
<code>const getComponents = (citations) => {
const components = {
a: (props) => {
const { href, children } = props;
const index = href.split(':')[1];
const { detail, goto } = citations.current?.[index] || {};
return (
<Link goto={goto} detail={detail}>{children}</Link>
);
}
};
return components;
};
</code>App component that polls the backend and uses
useRefto keep citation data stable across renders:
<code>import Markdown from 'react-markdown';
const defaultUrlTransform = (url) => url;
const App = () => {
const [data, setData] = useState({});
const citationsRef = useRef(data.citations);
useEffect(() => {
const interval = setInterval(() => {
fetchData().then(({ content, citations, status }) => {
setData({ content, citations });
if (status === 'finish') clearInterval(interval);
});
}, 1000);
return () => clearInterval(interval);
}, []);
const components = useMemo(() => getComponents(citationsRef), [citationsRef]);
citationsRef.current = data.citations || {};
return (
<Markdown
urlTransform={defaultUrlTransform}
components={components}
>
{data.content || ''}
</Markdown>
);
};
</code>The
useRefhook preserves the citation reference without triggering re‑renders, ensuring that Link components always have access to the latest citation data even while the user interacts with the drawer.
Alternative Implementation
Another approach moves the
Drawercomponent to the App level and passes a click handler down to
Link:
<code>import Markdown from 'react-markdown';
const defaultUrlTransform = (url) => url;
const getComponents = (onElementClick) => ({
a: (props) => {
const { children } = props;
const onClick = () => onElementClick(props);
return <Link onClick={onClick}>{children}</Link>;
}
});
const App = () => {
const [data, setData] = useState({});
const citationsRef = useRef(data.citations);
// ...fetching logic omitted for brevity
const onElementClick = useCallback(() => {
// Use citationsRef to retrieve citation details
}, [citationsRef]);
const components = useMemo(() => getComponents(onElementClick), [onElementClick]);
citationsRef.current = data.citations || {};
return (
<>
<Markdown urlTransform={defaultUrlTransform} components={components}>
{data.content || ''}
</Markdown>
<Drawer /* props omitted */ />
</>
);
};
</code>Summary
react-markdown allows custom rendering via the
componentsprop.
useRefis essential for preserving mutable data across renders, preventing unnecessary re‑rendering and ensuring UI consistency during dynamic updates.
Thoughtful component design and data management enable personalized Markdown rendering requirements.
KooFE Frontend Team
Follow the latest frontend updates
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.