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
While Mermaid is a powerful library that supports multiple chart types, integrating it with React presents some challenges:
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.
Here's how MermaidJS rendering works in React:
<svg class="mermaid">
elements.<svg class="mermaid">
elements with the startOnLoad
attribute set.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
If you don't have time or just simply want to quickly read the solution then let continue.
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
Because of behaviors like panning and zooming, combination This feature needs some adjustments
html2canvas
(https://www.npmjs.com/package/html2canvas/v/1.4.1) to create a canvas element from the SVG.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