当前位置: 首页 > news >正文

React + Mermaid 图表渲染消失问题剖析及 4 种代码级修复方案

Mermaid 是一个流行的库,它可以将文本图表(例如 graph LR; A-->B;)转换为 SVG 图表。

在静态 HTML 页面中,Mermaid 会查找 <pre class="mermaid"> 代码块,并在页面加载时将它们替换为渲染后的图表。

它甚至会添加一个特殊的 data-processed 属性来标记已转换的块。

然而,在 React 应用中,这可能会导致一个意外的 bug:

你的 Mermaid 图表最初显示正常,但一旦 React 重新渲染(例如状态变化后),图表就会消失,取而代之的是原始的 Mermaid 代码。

“Mermaid 图表在项目首次加载时渲染得非常完美,但如果我修改图表标记,它就会以纯文本形式渲染,而不是图表。”

为什么会这样?我们又该如何解决呢?

Mermaid 渲染的工作原理

在底层,Mermaid 通过扫描 DOM 来查找带有 class="mermaid" 的元素,并解析它们的文本。

例如,<pre class="mermaid">graph LR A-->B</pre> 将被替换为一个内联 SVG,显示该流程图。

在页面加载时(或手动触发时),Mermaid 的运行函数会遍历文档,将每个代码块转换为 SVG,并为这些元素添加 data-processed 标签,以避免再次渲染。

这就是 Mermaid 的正常生命周期:

  • 初始加载: Mermaid(通常通过 mermaid.init()、mermaid.contentLoaded() 或 mermaid.run())会找到所有 <pre class="mermaid">…</pre> 块,并将它们替换为 <svg> 图表。
  • 标记: 转换图表后,Mermaid 会添加 data-processed="true" 属性,这样它就知道不用再处理它了。
  • 重新渲染: 如果你后来添加更多 .mermaid 块并再次调用 mermaid.run(),它会跳过任何已标记的块。

data-processed 机制是性能的关键:它防止 Mermaid 在每次调用时重新解析每个图表。

但在 React 中,这个机制适得其反。

React 的虚拟 DOM 与 Mermaid 的冲突

React 使用自己的 虚拟 DOM 来高效更新 UI。

在 React 组件中,你返回的 JSX 描述了 UI 应该 是什么样子。

React 然后将这个虚拟树与实际 DOM 进行比较,并进行最小化更新。

关键在于,React 会覆盖它“拥有”的任何 DOM 部分

正如 React 文档所解释的,“React 会自动更新 DOM 以匹配你的渲染输出”。

换句话说,如果 Mermaid 直接修改了真实 DOM(通过插入 <svg>),React 并不知道;

下次 React 渲染该组件时,它会用渲染函数指定的内容替换那个 <svg>——在我们的案例中,很可能是原始的 <pre class="mermaid">…</pre> 文本。

换一种说法,Mermaid 操作的是 真实 DOM,而 React 维护的是 虚拟 DOM 并将其与真实 DOM 协调。

当两者冲突时,React 会获胜。

可以这么说:“Mermaid 直接与浏览器的真实 DOM 交互,这与 React 的虚拟 DOM 方法形成对比。”

具体来说,在 React 应用中的典型序列是:

  1. 初始挂载: React 渲染组件,包含 <div class="mermaid">…图表代码…</div>。然后我们触发 mermaid.contentLoaded() 或类似函数,Mermaid 将其转换为 DOM 中的 <svg>。
  2. 状态更新: 某些东西变化了(props 或状态),React 重新运行组件的渲染函数。如果该函数仍然返回原始的 <div class="mermaid">…图表代码…</div>,React 会用原始文本元素覆盖 SVG,因为那是虚拟 DOM 指定的内容
  3. 图表消失,原始文本重新出现。

这种交互就是 Mermaid 图表在更新时“消失”的原因:React 本质上抹除了 Mermaid 的工作。

要修复这个问题,我们需要将 Mermaid 的真实 DOM 渲染与 React 的渲染周期桥接起来。

策略 #1:移除 data-processed并重新运行 Mermaid

一个常见且直接的修复方法是 清除 data-processed 标志,并在图表数据变化时再次调用 Mermaid 的渲染

由于 Mermaid 不会重新渲染已标记的块,我们首先移除该属性,让它视之为新块。

例如:

import React, { useEffect } from 'react';
import mermaid from 'mermaid';/** 方法 1:移除 `data-processed` 并调用 contentLoaded。* 这确保 Mermaid 会重新扫描元素。*/
function MermaidChart({ chartDefinition }) {useEffect(() => {// 找到容器并移除 Mermaid 的标记属性const element = document.getElementById('mermaid-container');element?.removeAttribute('data-processed');// 重新运行 Mermaid 以重新渲染图表mermaid.contentLoaded();}, [chartDefinition]); // 每当 chartDefinition 变化时运行 effect// 在容器中渲染图表代码return (<div id="mermaid-container" className="mermaid">{chartDefinition}</div>);
}

在这个代码片段中,每次 chartDefinition prop 变化时,我们获取图表的 DOM 元素(#mermaid-container),移除其 data-processed 属性,并调用 mermaid.contentLoaded()。

这会“欺骗” Mermaid,让它再次查看该元素并重绘图表。

一篇 StackOverflow 回答总结了这种方法:“你需要在组件状态更新后移除该属性并重新调用 mermaid.contentLoaded()。”

这个 hack 在 React 更改底层文本时让 Mermaid 更新图表。

注意事项: 确保代码中的 ID 或类与你的目标匹配。还要注意 mermaid.contentLoaded() 会尝试重新渲染页面上的 所有 Mermaid 块,而不仅仅是一个,因此如果你有许多图表,这个方法可能会比较耗资源。对于少量图表来说,它很合适。

策略 #2:使用 mermaid.render()手动生成 SVG

另一种方法是完全绕过 Mermaid 的自动扫描,并 使用 mermaid.render() 手动生成 SVG 代码

而不是让 Mermaid 自己修改 DOM,你用图表文本调用 API,并获取 SVG 字符串作为返回。

然后,你可以将该字符串注入组件中,例如使用 dangerouslySetInnerHTML。

这样,React 保持对 DOM 的控制(它看到的是你设置在状态中的 <svg>),从而完全避免 data-processed 问题。

以下是一个使用现代 Mermaid API(返回 Promise)的 React 示例:

import React, { useState, useEffect } from 'react';
import mermaid from 'mermaid';/** 方法 2:使用 mermaid.render() 获取 SVG 并注入它。* 这显式生成图表代码。*/
function MermaidChart({ chartDefinition }) {const [svgCode, setSvgCode] = useState('');useEffect(() => {let isMounted = true; // 避免在卸载组件时更新状态async function renderChart() {try {await mermaid.parse(chartDefinition); // 可选:验证图表const { svg } = await mermaid.render('uniqueChartId', chartDefinition);if (isMounted) {setSvgCode(svg);}} catch (error) {console.error('渲染 Mermaid 图表出错:', error);}}renderChart();return () => {isMounted = false;};}, [chartDefinition]);// 直接将 SVG 字符串渲染到 DOM 中return <div dangerouslySetInnerHTML={{ __html: svgCode }} />;
}

工作原理: 每次 chartDefinition 变化时,我们解析并渲染它。mermaid.render('uniqueChartId', chartDefinition) 返回一个包含 svg 字段的对象(SVG 标记)。然后我们将该 SVG 存入 React 状态。组件输出一个 <div>,其 HTML 设置为 SVG。因为 React 直接渲染 SVG 标记,所以没有被擦除的风险——React 拥有该 SVG 节点。

这种模式在实践中被多位作者展示。

例如,一篇教程在 React 应用中使用 mermaid.render("theGraph", definition, (svgCode) => { output.innerHTML = svgCode; })。

Tuanhuy 博客也在 useLayoutEffect 中使用 const { svg } = await mermaid.render("id", graphText); 来设置状态变量。

关键点是:自己使用 Mermaid API,而不是依赖自动扫描。

注意: 如果使用此方法,确保 mermaid.render 的第一个参数(这里是 'uniqueChartId')对每个图表都是唯一的,因为 Mermaid 用它来标识 SVG 元素。在 React 中,如果你渲染多个图表,可以使用 ref 或 UUID。

策略 #3:使用 useLayoutEffect进行同步渲染

React 的 useEffect 钩子在组件更新浏览器后运行(绘制后)。

相反,useLayoutEffect 在 React 应用 DOM 更新后但浏览器重绘前运行。

这种时机在库(如 Mermaid)需要立即作用于 DOM 时更安全。

因为 Mermaid 期望真实 DOM 在绘制前就位,使用 useLayoutEffect 可以避免闪烁。

在实践中,你可以在 useLayoutEffect 中调用 mermaid.contentLoaded()。

例如:

import React, { useLayoutEffect } from 'react';
import mermaid from 'mermaid';/** 方法 3:使用 useLayoutEffect 初始化和渲染。* 这确保 Mermaid 在 React 更新 DOM 后运行。*/
function MermaidChart({ chartDefinition }) {// 一次性初始化 MermaiduseLayoutEffect(() => {mermaid.initialize({ startOnLoad: false });}, []);// 每当图表变化时重新运行 MermaiduseLayoutEffect(() => {// 当 React 用新 chartDefinition 更新 DOM 时,重新运行 Mermaidmermaid.contentLoaded();}, [chartDefinition]);return <div className="mermaid">{chartDefinition}</div>;
}

在这个示例中,第二个 useLayoutEffect 的依赖数组包含 chartDefinition,因此它会在 React 将新图表文本放入 DOM 后立即运行。

使用 useLayoutEffect(而非 useEffect)确保我们甚至不会短暂看到原始文本。

可以这么说:“Mermaid 基于真实 DOM 渲染,因此必须在页面渲染后发生,所以我们使用 useLayoutEffect 钩子来渲染。”

通过在 useLayoutEffect 中运行 mermaid.contentLoaded(),Mermaid 会看到更新的 <div> 并绘制图表。

在后续 React 更新中,第一个 effect(空依赖)不会重新运行初始化,而第二个 effect 会根据需要重新运行渲染。

另一个变体是将 useLayoutEffect 与其中的 mermaid.render() 结合(如 Tuanhuy 示例)。

本质是 useLayoutEffect 让 Mermaid 有机会在浏览器绘制前绘制,从而实现更平滑的更新。

策略 #4:使用 MutationObserver 观察 DOM 变化(高级)

作为可选或高级方法,你可以使用 MutationObserver API 来监视 DOM 变化,并在新图表代码出现时触发 Mermaid。

这更复杂,但适用于 Mermaid 块由某些渲染器深层插入的系统。

思路是观察容器元素,当添加新子节点时,在它们上调用 Mermaid。

例如:

import React, { useEffect } from 'react';
import mermaid from 'mermaid';/** 方法 4:使用 MutationObserver 检测新 Mermaid 块。*/
function MermaidWrapper() {useEffect(() => {const observer = new MutationObserver((mutationsList) => {for (const mutation of mutationsList) {if (mutation.addedNodes.length > 0) {// 添加了新内容;重新扫描 Mermaid 图表document.querySelectorAll('div.mermaid').forEach(el => {el.removeAttribute('data-processed');});mermaid.contentLoaded();break;}}});// 观察整个文档或特定容器observer.observe(document.body, { childList: true, subtree: true });return () => observer.disconnect();}, []);// ... 你的应用动态插入 <div class="mermaid"> 块 ...return <ContentWithMermaid />;
}

这里我们监视 document.body(或某个包装元素)的任何新子节点。

当出现 addedNodes 时,我们假设可能有新 Mermaid 图表。

我们然后清除所有 .mermaid div 的 data-processed,并调用 mermaid.contentLoaded()。

这确保即使动态插入的图表也能被渲染。

谨慎使用: MutationObserver 对于大多数应用来说可能是多余的,如果误用可能会影响性能。但如果你的应用渲染流程难以仅用钩子拦截,这是一个选项。

结论

总之,React 中的 Mermaid 图表在重新渲染时消失是因为 React 的虚拟 DOM 用原始文本替换了 Mermaid 注入的 SVG。

要修复这个问题,我们需要在 React 更新后显式重新触发 Mermaid。

常见解决方案包括:

  • 清除 data-processed 并重新运行: 移除标记并在 React effect 中调用 mermaid.contentLoaded()(或 mermaid.run())。
  • 使用 mermaid.render() 用 API 生成 SVG 并让 React 渲染它(如上所示)。
  • 使用 useLayoutEffect 在布局 effect 中调用 Mermaid,让它在重绘前看到更新的 DOM。
  • (高级)MutationObserver: 监视 DOM 变化并根据需要触发 Mermaid。

每种方法都有权衡。

直接清除 data-processed 对于简单案例来说快速且容易,而 mermaid.render() 提供更多控制,但需要手动注入 HTML。

使用 React effect(useLayoutEffect)可以优雅地将 Mermaid 集成到 React 生命周期中。

借助这些策略,你可以让 Mermaid 图表在 React UI 更新时保持活跃。

参考资料:

更多细节请参阅 Mermaid 文档和社区关于将 Mermaid 与 React 集成的帖子。

http://www.dtcms.com/a/286720.html

相关文章:

  • 前端-CSS盒模型、浮动、定位、布局
  • 前端迟迟收不到响应,登录拦截器踩坑!
  • 比较含距离和顺序的结构相似性
  • 【EPLAN 2.9】许可证xx成功却显示红色叉,无法启动
  • 人工智能时代对高精尖人才的需求分析
  • 嵌入式数据结构之顺序表总结
  • openpyxl 流式读取xlsx文件(read_only=true)读不到sheet页中所有行
  • 配置本地git到gitlab并推送
  • 【机器学习】AdamW可调参数介绍及使用说明
  • 【LINUX操作系统】ssh远程连接---客户端Windows连接服务端虚拟机
  • 应用集成体系深度解析:从数据互通到流程协同
  • 你需要了解的 AI 智能体设计模式
  • compose multiplatform 常用库
  • Python FastMCP:让你的AI工具链飞起来
  • 深入解析操作系统中的文件控制块(FCB):从原理到现代实现演进
  • 利用动画实现热点图转圈循环放大效果
  • 深入理解 slab cache 内存分配全链路实现
  • 445、两数相加 II
  • 数字人直播:开启直播行业新纪元​
  • 基于LiteNetLib的Server/Client Demo
  • Android各版本适配方案总结归纳
  • 企业网站建设全攻略
  • Linux系统之:进程概念
  • JavaSE -- 对象序列化和反序列化详细讲解
  • HarmonyOS-ArkUI Web控件基础铺垫4--TCP协议- 断联-四次挥手解析
  • 全国计算机等级考试二级题库【C语言】:程序修改题型——结构体、可变数组、链表 自制答案详解合辑
  • 深度学习入门-深度学习简介
  • 屏显智能电子锁语音芯片方案新选择
  • Hinge Loss(铰链损失函数)详解:SVM 中的关键损失函数
  • C++实现单层时间轮