はじめに
ブログ記事やドキュメント、チャットの返信など、Markdownで書かれたテキストをReactアプリ内で表示したい場面は少なくありません。手っ取り早い方法としてmarkedやmarkdown-itでHTML文字列に変換し、dangerouslySetInnerHTMLで流し込むやり方がありますが、この方法にはユーザー入力由来のMarkdownを扱う際にXSS(クロスサイトスクリプティング)のリスクが常につきまといます。
この問題をライブラリ側で構造的に解決してくれるのが、今回紹介するreact-markdownです。HTML文字列を経由せず、MarkdownをそのままReactの要素ツリーに変換してくれるため、安全かつReactらしい書き方でMarkdown描画を実現できます。
react-markdownとは
react-markdownは、MarkdownテキストをReactコンポーネントとして描画するためのライブラリです。内部ではMarkdownパーサーのremarkと、HTML変換のrehypeからなるunifiedエコシステムを利用しており、パース結果を直接React要素に変換するため、中間生成物としての生HTML文字列が発生しません。
主な特徴
- XSSに強い設計 -
dangerouslySetInnerHTMLを使わずにReact要素へ直接変換するため、ユーザー投稿のMarkdownを扱う場合でも安全性が高いです(生のHTMLタグはデフォルトで無視されます)。 - 豊富なプラグインエコシステム - remark/rehypeのプラグインをそのまま利用できるため、GitHub Flavored Markdown(表・チェックリスト・打ち消し線)やシンタックスハイライト、数式表示などを柔軟に追加できます。
- 描画コンポーネントのカスタマイズ -
componentsプロパティで、見出しや画像、リンクなど任意のMarkdown要素を独自のReactコンポーネントに差し替えられます。
インストール
npm、yarn、pnpmのいずれでも導入できます。
# npm
npm install react-markdown
# yarn
yarn add react-markdown
# pnpm
pnpm add react-markdown
GitHub Flavored Markdown(表やタスクリストなど)を使う場合は、remark-gfmもあわせてインストールします。
npm install remark-gfm
基本的な使い方
Markdownコンポーネントに文字列を渡すだけで、Reactコンポーネントとして描画されます。
import Markdown from "react-markdown";
const markdown = `
# react-markdownへようこそ
これは**太字**と*斜体*のサンプルです。
- リスト項目1
- リスト項目2
`;
function App() {
return <Markdown>{markdown}</Markdown>;
}
export default App;
これだけで、見出しや段落、リストがそれぞれ対応するHTMLタグ(h1、p、ulなど)を持つReact要素として描画されます。
remark-gfmでテーブルやチェックリストに対応する
標準のMarkdown仕様では表やタスクリストは扱えないため、remark-gfmプラグインをremarkPluginsに渡して拡張します。
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
const markdown = `
| ライブラリ | 用途 |
| --- | --- |
| react-markdown | Markdown描画 |
| remark-gfm | GFM拡張 |
- [x] インストール
- [ ] プラグイン設定
`;
function App() {
return <Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>;
}
export default App;
実践的なユースケース
独自コンポーネントへの差し替え
componentsプロパティを使うと、特定のMarkdown要素だけデザインを変更できます。たとえばリンクを新しいタブで開くようにしたり、コードブロックにシンタックスハイライトを適用したりする際に便利です。
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown";
const components: Components = {
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
),
code: ({ className, children }) => (
<code className={`inline-code ${className ?? ""}`}>{children}</code>
),
};
function ArticleBody({ content }: { content: string }) {
return (
<Markdown remarkPlugins={[remarkGfm]} components={components}>
{content}
</Markdown>
);
}
export default ArticleBody;
危険なHTMLタグを許可しないまま安全に運用する
外部(ユーザー投稿やAPIレスポンス)から受け取ったMarkdownを表示する場合でも、react-markdownはデフォルトで生のHTMLタグを無視するため、追加の対策なしに一定の安全性が確保されます。仕様上どうしてもHTMLの埋め込みが必要な場合はrehype-rawとrehype-sanitizeを併用し、許可するタグ・属性を明示的に制限するのが定石です。
import Markdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
function TrustedButSanitizedContent({ content }: { content: string }) {
return (
<Markdown rehypePlugins={[rehypeRaw, rehypeSanitize]}>
{content}
</Markdown>
);
}
export default TrustedButSanitizedContent;
このように、生HTMLを許可する場合でもrehype-sanitizeでホワイトリスト方式に制御できるため、利便性と安全性のバランスを取りやすいのもreact-markdownの強みです。
まとめ
react-markdownは、dangerouslySetInnerHTMLに頼らずMarkdownをReact要素へ変換できる、安全性と拡張性を兼ね備えたライブラリです。remark-gfmでGFM記法に対応し、componentsプロパティで見た目を自由にカスタマイズできるため、ブログのCMS表示からチャットアプリのメッセージ描画まで幅広い用途に活用できます。
すでにMarkdownをHTML文字列変換で表示している場合は、セキュリティ面のリスクを減らす意味でもreact-markdownへの移行を検討してみてはいかがでしょうか。