当前位置: 首页 > 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.lryc.cn/news/592761.html

相关文章:

  • Java异步日志系统性能优化实践指南:基于Log4j2异步Appender与Disruptor
  • Camera相机人脸识别系列专题分析之十七:人脸特征检测FFD算法之libhci_face_camera_api.so 296点位人脸识别检测流程详解
  • CentOS 7 配置环境变量常见的4种方式
  • 虚拟机centos服务器安装
  • 机器人行业10年巨变从协作机器人到具身智能的萌芽、突破和成长——从 Automatic慕尼黑10 年看协作机器人到具身智能的发展
  • 低代码可视化工作流的系统设计与实现路径研究
  • Linux基础开发工具
  • 智合同丨当AI成为法律人的助手:合同审查效率变革观察
  • 代码随想录算法训练营第二十四天
  • Linux学习之认识Linux的基本指令
  • Linux 环境下 NTP 时间同步与 SSH 免密登录实战
  • 函数返回值问题,以及返回值的使用问题(c/c++)
  • RWA是什么意思?
  • 李天意考研数学精讲课学习笔记(课堂版)
  • elementui-admin构建
  • MBIST - Memory BIST会对memory进行清零吗?
  • PHP 8.0 升级到 PHP 8.1
  • 机器学习17-Mamba
  • 2025年UDP应用抗洪指南:从T级清洗到AI免疫,实战防御UDP洪水攻击
  • 从0开始学习R语言--Day50--ROC曲线
  • C语言—如何生成随机数+原理详细分析
  • 系统IO对于目录的操作
  • 服务器内存满了怎么清理缓存?
  • 多线程-4-线程池
  • 从零构建监控系统:先“完美设计”还是先“敏捷迭代”?
  • 内存数据库的持久化与恢复策略:数据安全性与重启速度的平衡点
  • 数据结构-3(双向链表、循环链表、栈、队列)
  • SGLang 推理框架核心组件解析:请求、内存与缓存的协同工作
  • 【PTA数据结构 | C语言版】左堆的合并操作
  • LS-DYNA分析任务耗时长,如何避免资源浪费与排队?