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.pswp.cn/diannao/91673.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/91673.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Element]修改el-pagination背景色

[Element]修改el-pagination背景色 代码 <el-pagination:current-page.sync"queryParams.current":page-size.sync"queryParams.size":page-sizes"[10, 20, 50, 100]"layout"prev, pager, next, jumper, sizes":total"queryP…

Docker 可用镜像列表

Docker 镜像源列表&#xff08;7月15日更新-长期&免费&#xff09;_dockerhub国内镜像源列表-CSDN博客

低代码可视化工作流的系统设计与实现路径研究

一、背景分析在数字化转型不断深化的背景下&#xff0c;企业业务流程呈现出高度定制化与动态调整的趋势&#xff0c;传统信息系统在开发周期、实施成本与扩展能力上的局限性日益凸显&#xff0c;已难以支撑快速响应和敏捷迭代的实际需求。面向这一现实挑战&#xff0c;基于 BPM…

mac mlx大模型框架的安装和使用

mlx是apple平台的大模型推理框架&#xff0c;对mac m1系列处理器支持较好。 这里记录mlx安装和运行示例。 1 安装mlx框架 conda create -n mlx python3.12 conda activate mlx pip install mlx-lm 2 运行mlx测试例 以下是测试程序&#xff0c;使用方法和hf、vllm等推理框架基…

JAVA 使用Apache POI合并Word文档并保留批注的实现

一、需求背景 在实际工作中&#xff0c;我们经常需要将多个Word文档合并成一个文件。但当文档中包含批注&#xff08;Comments&#xff09;时&#xff0c;传统的复制粘贴会导致批注丢失或引用错乱。本文将介绍如何通过Java和Apache POI库实现保留批注及引用关系的文档合并功能。…

Linux的服务管理工具:`systemd`(`systemctl`)和`SysVinit ` 笔记250718

Linux的服务管理工具:systemd(systemctl)和SysVinit 笔记250718 Linux的服务管理工具 Linux 的服务管理工具随着发行版和初始化系统的发展而演变。以下是主要的服务管理工具及其对应的初始化系统&#xff1a; 1. systemd (现代主流标准) 初始化系统&#xff1a; 是绝大多数…

Couchbase 可观测性最佳实践

Couchbase 介绍 Couchbase 是一个开源的分布式 NoSQL 数据库&#xff0c;专为高性能和高可扩展性设计&#xff0c;适用于实时数据处理的企业应用。它结合键值存储和文档数据库的优势&#xff0c;支持 JSON 文档存储&#xff0c;并通过 N1QL&#xff08;类 SQL 查询语言&#x…

构建基于MCP的LLM聊天机器人客户端开发指南

引言 在当今人工智能技术快速发展的时代&#xff0c;大型语言模型(LLM)已成为构建智能应用的核心组件。MCP(Modular Conversational Platform)作为一个强大的对话平台&#xff0c;为开发者提供了将LLM能力与自定义工具集成的标准化方式。本文将详细介绍如何使用Python开发一个…

接口测试的原则、用例与流程详解

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、接口的介绍软件测试中&#xff0c;常说的接口有两种&#xff1a;图形用户接口&#xff08;GUI&#xff0c;人与程序的接口&#xff09;、应用程序编程接口&am…

ubuntu 22.02 带外进单用户拯救系统

不停地按 F7 &#xff0c;然后进到 menu &#xff0c;选择 ubuntu &#xff0c;然后按下 ESC &#xff0c;然后瞬间会刷一个 ubuntu 的选项&#xff08;默认是在第一的位置&#xff0c;直接快速按下 e&#xff09;即可进入单用户模式。 找到类似 linux /boot/vmlinuz-xxx rootU…

Java-75 深入浅出 RPC Dubbo Java SPI机制详解:从JDK到Dubbo的插件式扩展

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; AI炼丹日志-30-新发布【1T 万亿】参数量大模型&#xff01;Kim…

【橘子分布式】gRPC(编程篇-上)

一、简介 我们之前学习了grpc的一些理论知识&#xff0c;现在我们开始正式进入编程环节。 我们的项目结构和之前的thrift结构还是一样的&#xff0c;一个common,一个client&#xff0c;一个server。只不过在grpc这里common它一般叫做api模块。还是放置一些公共的实体类&#x…

IOS 18下openURL 失效问题

突然有一天有玩家反馈说应用打开外部连接打不开了&#xff0c;于是查了一下&#xff0c;报错&#xff1a;BUG IN CLIENT OF UIKIT: The caller of UIApplication.openURL(_:) needs to migrate to the non-deprecated UIApplication.open(_:options:completionHandler:). Force…

前端面试题(React 与 Vue)

目录 一、React 函数组件 Fiber架构 组件重新渲染 组件通信 为什么不能在if中使用hook useEffect与useLayoutEffect区别 性能优化hooks 受控组件与非受控组件 redux与zustand区别 二、Vue vue2与vue3区别 生命周期 computed与watch区别 v-if与v-show区别 v-mod…

大模型格式

目录 大模型格式&#xff1a; ollma 可以加载gguf ChatGPT 说&#xff1a; &#x1f50d; 什么是 GGUF&#xff1f; 大模型格式&#xff1a; Ollama 模型格式只能运行已打包成 .gguf 格式的模型&#xff0c;或通过其 Modelfile 方式构建 ModelScope 模型格式大多使用 Hug…

数据结构 栈(1)

1. 栈的概念和结构之前几篇我们分别讲解了顺序表和单链表的内容&#xff0c;今天我们又来学习一个新的关于数据结构的内容--- 栈 。栈&#xff1a;栈也属于线性表 , 但它是一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一…

【Android代码】绘本翻页时通过AI识别,自动通过手机/pad朗读绘本

核心功能&#xff1a; 打开摄像头&#xff08;可支持外接摄像头&#xff09;检测翻页&#xff08;后续考虑添加图像差异算法&#xff09;拍照后用 识图自动用 TextToSpeech 朗读文字内容 &#x1f4cc; 说明&#xff1a;使用了 CameraX&#xff08;Android Jetpack&#xff09;…

园区IPv6规划与部署

​今天我将围绕“园区IPv6规划与部署”这一主题&#xff0c;结合行业趋势、技术难点和实际案例&#xff0c;与大家分享一套可落地的规划方法论。​在开始前&#xff0c;我想先问大家一个问题&#xff1a;​如果现在让你给一个新建园区设计网络&#xff0c;你会优先考虑IPv4还是…

mingw11.2+opencv4.12 cmake contrib编译

第一次Configure之后&#xff0c;会出现不少错误&#xff0c;主要是因为文件没办法正常下载引起的,因为之前编译过vs2022 ,缓存里面有应该下载的文件了&#xff0c;所以这次没有错误&#xff0c;如果你第一次Configure有下载错误&#xff0c;可以下载以下的文件飞书 Docs Link:…

免费MCP服务:Excel CSV 转 JSON MCP by WTSolutions 文档

简介 Excel 转 JSON MCP&#xff08;模型上下文协议&#xff09;提供了一个标准化接口&#xff0c;用于通过模型上下文协议将 Excel 和 CSV 数据转换为 JSON 格式。此 MCP 实现提供了两个专门用于数据转换的工具&#xff1a; excel_to_json_mcp_from_data&#xff1a;转换制表…