Markdown, MermaidをReactアプリ内に表示したい
やりたいこと
React で GUI アプリケーションを作成していますが、API もユーザーに公開しています。
ユーザーへ公開するためのドキュメントは別管理していましたが、アプリと合わせて更新できた方が更新忘れもなくリリース対象も減るので、API リファレンスもアプリケーション内に掲載するようにしました。API リファレンスの掲載についてはこちらでできました。
このとき、API 利用方法などを記載したドキュメントも公開したくなりました。文章による説明に加え、シーケンス図も掲載すると理解しやすそうです。
普通に HTML で文章を書いて、図は画像で埋め込むことでも実現できますが、更新が面倒そうなので Markdown + Mermaid で書いたドキュメントをそのまま表示したいです。
動作確認環境
パッケージ | バージョン |
---|---|
node | 22.11.0 |
yarn | 1.22.17 |
React | 18.3.1 |
vite | 6.0.1 |
Typescript | 5.6.2 |
手順
React をセットアップ
React + Vite +Typescript でプロジェクトを作成します。
プラグインをインストール
今回使うプラグインは4つです。
Mermaid が不要であれば上3つで良いです。
プラグイン名 | 用途 |
---|---|
react-markdown | Markdown 記法を解釈して HTML 出力する |
remark-gfm | GFM (github flavored markdown) で解釈します。これがないと表が使えません |
react-syntax-highlighter | コードハイライト |
mermaid | Mermaid.js の記述を解釈します |
$ yarn add react-markdown remark-gfm react-syntax-highlighter
info Direct dependencies
├─ mermaid@11.4.1
├─ react-markdown@9.0.1
├─ react-syntax-highlighter@15.6.1
└─ remark-gfm@4.0.0
Typescript 用に型定義もインストールします。
$ yarn add -D @types/react-syntax-highlighter
info Direct dependencies
└─ @types/react-syntax-highlighter@15.5.13
本筋ではないですが、ルーティング用に react-router-dom もインストールします。
$ yarn add react-router-dom
info Direct dependencies
└─ react-router-dom@7.0.1
実装
ドキュメントは .md ファイルとして作成します。
このファイルを読み込んでアプリケーション内の1ページに表示します。
できあがった全体を示します。
ファイル構成
├── eslint.config.js
├── index.html
├── package.json
├── public
│ └── api
│ └── guide.md // 追加
├── src
│ ├── index.css
│ ├── main.tsx // 変更
│ ├── pages
│ │ └── ApiGuide
│ │ ├── Mermaid.tsx // 追加
│ │ └── index.tsx // 追加
│ └── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── yarn.lock
public/api/guide.md
いつも通り Markdown で書いたファイルを配置します。
(この記事中に Markdown コードを入れるにはどうしたら良いのだろう)
src/main.tsx
今回作ったページへのルーティングを追加。
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
+ import { BrowserRouter, Route, Routes } from 'react-router-dom'
+ import ApiGuide from './pages/ApiGuide/index.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
+ <BrowserRouter>
+ <Routes>
+ <Route path="/api-guide" element={<ApiGuide />} />
+ </Routes>
+ </BrowserRouter>
</StrictMode>,
)
src/pages/ApiGuide/index.tsx
import mermaid from "mermaid";
import React from "react";
import Markdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import remarkGfm from "remark-gfm";
import { Mermaid } from "./Mermaid";
const ApiGuide: React.FC = () => {
const [markdown, setMarkdown] = React.useState("");
// ① 表示したい Markdown ファイルを読み込む
React.useEffect(() => {
(async () => {
const file = await fetch("/api/guide.md");
setMarkdown(await file.text());
mermaid.initialize({ startOnLoad: false });
})();
}, []);
return (
<Markdown
remarkPlugins={[remarkGfm]}
components={{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
code({ node, className, children, ref, ...props }) {
if (
// ② Mermaid のコードブロックの場合は Mermaid コンポーネントで表示
className === "language-mermaid" &&
node?.children[0].type === "text"
) {
return <Mermaid code={node?.children[0].value} />;
} else {
// ② Mermaid でないコードブロックの場合は react-syntax-highlighter で表示
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
// ③ style はお好みで
// eslint-disable-next-line @typescript-eslint/no-explicit-any
style={vscDarkPlus as any}
language={match[1]}
// ④ デフォルトだと code > code と二重タグになる
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
) : (
<code className={className}>{children}</code>
);
}
},
}}
>
{markdown}
</Markdown>
);
};
export default ApiGuide;
② の部分でコードブロックを判別し、Mermaid 記法が指定されていたら次の Mermaid コンポーネントが利用されるようにしています。
③ スタイルはお好みで変えてください。指定できるスタイルはこちら。
src/pages/ApiGuide/Mermaid.tsx
import mermaid from "mermaid";
import React from "react";
type Props = {
code: string;
};
export const Mermaid: React.FC<Props> = (props) => {
const { code } = props;
const outputRef = React.useRef<HTMLDivElement>(null);
const render = React.useCallback(async () => {
if (outputRef.current && code) {
try {
// ① 一意な ID を指定する必要あり
const { svg } = await mermaid.render(`m${crypto.randomUUID()}`, code);
outputRef.current.innerHTML = svg;
} catch (error) {
console.error(error);
outputRef.current.innerHTML = "Invalid syntax";
}
}
}, [code]);
React.useEffect(() => {
render();
}, [render]);
return code ? (
<div style={{ backgroundColor: "#fff" }}>
<div ref={outputRef} />
</div>
) : null;
};
Mermaid コードブロックの際に表示されるコンポーネントです。
① の部分でレンダリングに ID を指定する必要があるのですが、ページに複数の Mermaid コードブロックが存在する場合、ID が重複していると正常に表示されません。このため、ユニーク ID を指定しています。
randomUUID では数字はじまりの ID も生成されますが、HTML のルールで英字から始まる必要があり、数字はじまりの ID だと表示も正常にできませんでした。このため、先頭に英字を追加しています。
そのほか
remarkGfm により表の記法は解釈されて Table タグで出力されますが、スタイルが当たらないため見づらいです。
スタイルは別途指定する必要があるので、プロダクトのスタイル定義方法に合わせてページに書くなりグローバルに設定するなりが必要です。
Zenn っぽくするとこんな感じのスタイルを当てれば良いです。
table: {
borderCollapse: "collapse",
},
th: {
padding: ".5rem",
fontWeight: 700,
border: "1px solid #d6e3ed",
backgroundColor: "#edf2f7",
},
td: {
padding: ".5rem",
border: "1px solid #d6e3ed",
backgroundColor: "#fff",
}
まとめ
若干はまりどころもありましたが、少し手を加えて表示できました。
図をスクショしてページ内に表示・・・とかしなくて良くなったので管理が楽です。
今回はシーケンス図しか使いませんでしたが、Mermaid で記述できるものはなんでも表示できるはずです。
Discussion