Integrate MermaidJS with ReactJS

Integrate MermaidJS with ReactJS

MermaidJS is one of the most useful tools for visualizing your ideas, charts, and data. In my current generative AI application, it combines MermaidJS with ReactJS and React Query. There wasn't any guidance available online when I implemented it, so today's post details how I integrated MermaidJS + React + React Query.

First we need to install these package by :

1npm install @tanstack/react-query react react-dom mermaid @types/mermaid typescript 2

For different app's purposes, I will create a <MermaidRenderer /> for rendering MermaidJS library. it includes these steps:

1import mermaid, { MermaidConfig } from 'mermaid'; 2 3export interface MermaidRendererProps { 4 graphText: string; 5} 6 7export const MermaidRenderer: React.FC<MermaidRendererProps> = ({ graphText }) => { 8 9 return ( 10 // the implementation goes here 11 ); 12} 13

1.Challenges of the integration

While Mermaid is a powerful library that supports multiple chart types, integrating it with React presents some challenges:

  • No Official Integration Documentation: There are no official integration documents for ReactJS.
  • RealDOM vs. Virtual DOM: Mermaid interacts directly with the browser's Real DOM, contrasting with React's Virtual DOM approach for efficient re-rendering.
  • Performance Considerations: The package can be resource-intensive, especially in scenarios involving multiple or complex charts within a generative AI application.
  • Additional Requirements: Features like copying charts as code, downloading rendered charts, and zooming require additional work.

These requirements somewhat headache to me. But we will resolve it together. before any requirement, how to render mermaid in ReactJS's world. I've researched for days and found out how to do it.

2.Render mermaidJS in ReactJS

Here's how MermaidJS rendering works in React:

  1. Browser Renders HTML: The browser renders the initial HTML code, including potential <svg class="mermaid"> elements.
  2. Mermaid Initialization: When the page loads, Mermaid can leverage two rendering options:
    • Automatic Rendering: It renders all <svg class="mermaid"> elements with the startOnLoad attribute set.
    • Manual Rendering (Preferred): We use this approach to render on demand using Mermaid's API methods.

you can read more about the configuration here https://mermaid.js.org/config/setup/interfaces/mermaid.MermaidConfig.html

1import mermaid, { MermaidConfig } from 'mermaid'; 2 3const mermaidConfig: MermaidConfig = { 4 startOnLoad: false, // this is important to stop rendering chart on page load (reactjs re-renders multiple times). 5 theme: 'default', 6 flowchat: {...}, 7 sequence: {...}, 8 pie: {...} 9} 10mermaid.initialize(mermaidConfig); 11

Because the mermaid renders based on RealDOM so it must be happened after the page rendered, so we will use useLayoutEffect hook for this render.

1import mermaid, { MermaidConfig } from 'mermaid'; 2 3export interface MermaidRendererProps { 4 graphText: string; 5} 6 7export const MermaidRenderer: React.FC<MermaidRendererProps> = ({ graphText }) => { 8 const [graph, setGraph] = React.useState<string>(''); 9 10 const renderGraph = useCallback(async () => { 11 try { 12 const isChartValid = await mermaid.parse(graphText); 13 if (!isChartValid) throw new Error("Invalid chart"); 14 const { svg: svgCode }: RenderResult = await mermaid.render("id", graphText); 15 setChart(svgCode); 16 } catch (error) { 17 // TODO: Handle your custom error 18 console.error(error); 19 } finally { 20 setLoading(false); 21 } 22 }, [graphText]); 23 24 useLayoutEffect(() => { 25 renderGraph(); 26 }, [renderGraph]); 27 28 return ( 29 <div className={styles.svgContainer} dangerouslySetInnerHTML={{__html: chart ?? ""}} /> 30 ); 31} 32

Okay then we are able to render the chart. Homework for you :D

  • Implement loading state
  • Handle error
  • Zoom and span svg.

If you don't have time or just simply want to quickly read the solution then let continue.

3.Pan and Zoom

For this requirement, I use svg-pan-zoom package (https://www.npmjs.com/package/svg-pan-zoom). Then we can simply make a configuration and use svgPanZoom method from the package to apply this feature.

1const svgPanZoomConfig: SvgPanZoom.Options = { 2 panEnabled: true, 3 zoomEnabled: true, 4 fit: true, 5 center: true, 6 minZoom: 0.5, 7 maxZoom: 5, 8 zoomScaleSensitivity: 0.1, 9 dblClickZoomEnabled: true, 10 preventMouseEventsDefault: true, 11}; 12export const MermaidRenderer: React.FC<MermaidRendererProps> = ({ graphText }) => { 13 const containerRef = useRef<HTMLDivElement | null>(null); 14 const [isLoading, setLoading] = useState<boolean>(true); 15 16 /* ... */ 17 18const applyPanAndZoom = useCallback(() => { 19 if (containerRef.current && chart) { 20 const svgElement = containerRef.current.querySelector("svg"); 21 if (svgElement) { 22 const panZoomInstance = svgPanZoom(svgElement, svgPanZoomConfig); 23 panAndZoomInstanceRef.current = panZoomInstance; 24 } 25 } 26 }, [chart]); 27 28 useLayoutEffect(() => { 29 applyPanAndZoom(); 30 }, [applyPanAndZoom]); 31 32 return ( 33 <div className={styles.container} ref={containerRef}> 34 { isLoading 35 ? <div>Loading...</div> 36 : <div ref={svgContainerRef} dangerouslySetInnerHTML={{__html: chart ?? ""}} />} 37 </div> 38 ) 39}; 40

3.Download feature

Because of behaviors like panning and zooming, combination This feature needs some adjustments

1 const resetPanAndZoom = useCallback(() => { 2 const panZoomInstance = panAndZoomInstanceRef.current; 3 if (chart && panZoomInstance) { 4 panZoomInstance.reset(); 5 } 6 }, [chart]); 7 8 const downloadByDataUrl = useCallback((dataUrl: string, name: string = "graph") => { 9 const link = document.createElement("a"); 10 link.href = dataUrl; 11 link.download = `${name}.png`; 12 link.click(); 13 }, []); 14 15 const downloadGraph = useCallback(async () => { 16 resetPanAndZoom(); 17 if (svgContainerRef.current && chart) { 18 const canvas = await html2canvas((svgContainerRef.current as HTMLElement), { backgroundColor: 'transparent' }); 19 if (canvas) { 20 const dataUrl = canvas.toDataURL("image/png"); 21 downloadByDataUrl(dataUrl); 22 } 23 } 24 25 }, [chart, downloadByDataUrl, resetPanAndZoom]); 26

Well, If you go to this point, I hope that the solution make your life easier or helpful at least. The complete, working solution is available on my GitHub for your reference https://github.com/tuanhuydev/mermaid-react-integration. As always, I'm tuanhuydev, a professional software engineer from Vietnam. I'm passionate about contributing to projects and businesses, so if you have any needs, feel free to reach out. Thank you for your read and I will see you in the next post.#tuanhuydev