Frontend Development 9 min read

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.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
Customizing Markdown Rendering with react-markdown and useRef

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

content

field 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

useRef

to 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

useRef

hook 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

Drawer

component 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

components

prop.

useRef

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

FrontendJavaScriptuseRefcustom renderingreact-markdown
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.