---
title: "react-markdownの使い方｜GFM表示とrehype-sanitizeで安全にMarkdownを描画する"
url: "https://www.issoh.co.jp/tech/details/3660/"
published: 2024-09-25
updated: 2026-07-23
categories: ["React"]
publisher: "株式会社一創"
---

# react-markdownの使い方｜GFM表示とrehype-sanitizeで安全にMarkdownを描画する

react-markdownは、Markdown文字列を`dangerouslySetInnerHTML`を使わずにReactの要素ツリーへ変換して描画するライブラリです。生成AIの出力やCMSの本文をそのまま画面に出す場面で、XSSを避けながらMarkdownを表示できます。この記事では、react-markdown v10（10.0.0は2025年2月公開）を前提に、インストールからGFMのテーブル表示、rehype-sanitizeによる安全なHTML描画、カスタムコンポーネント、見出しへジャンプする目次までを最小コードで整理します。ReactとNext.jsの関係を先に押さえたい場合は[Next.jsとNuxtの違いを比較｜React・Vueとの関係と選び方](/tech/details/2832/)も参照してください。

## まとめ：react-markdown運用の要点

- インストールは`npm install react-markdown`。テーブルなどGFMを使うなら`remark-gfm`を併せて入れ、`remarkPlugins`に渡す。
- react-markdownは既定でHTMLをエスケープするため安全。生HTMLを描画するときだけ`rehype-raw`を使い、必ず`rehype-sanitize`と組み合わせて無害化する。
- v10.0.0で`className`プロップが廃止。スタイルは外側の要素か`components`で当てる。
- 見出しのIDは`rehype-slug`で自動付与でき、そのIDへ`#`リンクでジャンプする目次を作れる。

以下で各手順とコードを順に説明します。

## react-markdownとは：仕組みと採用理由

react-markdownはMarkdown文字列を解析し、いったんhast（HTMLの抽象構文木）へ変換してからReact要素として出力します。文字列をHTMLとして直接DOMへ挿入しないため、`dangerouslySetInnerHTML`で起きがちなスクリプト注入（XSS）を構造的に避けられるのが最大の採用理由です。remark／rehypeのプラグイン機構に対応し、GFMやサニタイズ、見出しID付与などを組み合わせて拡張できます。v10はESMとReact 18以降を前提とする点に注意してください。

## インストールと基本的な表示

### インストールコマンド

本体に加え、テーブルやタスクリストを扱うためのGFMプラグイン、後述の安全なHTML描画に使うプラグインをまとめて導入します。

```
npm install react-markdown remark-gfm rehype-sanitize rehype-raw rehype-slug
```

最小構成でよければ`react-markdown`単体でも動きます。追加機能が要るときにプラグインを足す方針が扱いやすいです。

### 最小コードでのMarkdown描画

Markdown文字列を`children`として渡すだけで描画できます。

```
import Markdown from 'react-markdown';

export default function MarkdownView({ source }) {
  return <Markdown>{source}</Markdown>;
}
```

Next.jsのApp Routerでは、Markdownの整形にNode APIを使わないこのコンポーネントはServer / Client どちらでも配置できます。

### バージョン差の落とし穴（className廃止・urlTransform統合）

旧記事や古いサンプルの多くはv8以前の書き方で、いくつかのプロップがバージョンごとに姿を変えています。トップレベルの`className`プロップは10.0.0（2025年2月）で廃止され、`transformLinkUri`・`transformImageUri`は9.0.0（2023年9月）で`urlTransform`へ統合されました。ラッパーにクラスを当てたい場合は、`<Markdown>`を自前の`div`で囲むか、`components`で各要素に付与します。`className`が効かないという報告の多くは、10.0.0での廃止に古いサンプルが追随していないことが原因です。

## GFMのテーブル・タスクリスト表示（remark-gfm）

標準のMarkdownはテーブルや打ち消し線、タスクリストを解釈しません。GitHub Flavored Markdown（GFM）を有効にするには`remark-gfm`を`remarkPlugins`へ渡します。「react-markdownでテーブルが表示されない」原因のほとんどはこのプラグイン未導入です。

```
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

<Markdown remarkPlugins={[remarkGfm]}>{source}</Markdown>
```

これで`| 列 | 列 |`記法がHTMLの`table`として描画されます。テーブルの見た目は、生成される`table`要素にCSSを当てて整えます（枠線・ゼブラ・横スクロールなど）。react-markdownはインラインスタイルを付けないため、スタイリングはアプリ側のCSSで完結させます。

## rehype-sanitizeによる安全なHTML表示（XSS対策）

### なぜdangerouslySetInnerHTMLではなくreact-markdownか

ユーザー投稿や外部データをそのまま`dangerouslySetInnerHTML`で挿入すると、`<script>`や`onerror`属性経由でスクリプトが実行されるXSSの入口になります。react-markdownは既定でMarkdown内の生HTMLをエスケープするため、この既定のままなら安全です。XSSとCSRFの違いや実装レベルの対策は[XSSとCSRFの違いとは？仕組み・被害・対策を実装レベルで徹底比較](/tech/details/4109/)で整理しています。

### 生HTMLの許可と無害化（rehype-raw + rehype-sanitize）

Markdown内に書かれた生HTMLを描画したい場合は`rehype-raw`で解析を有効化します。ただしこれ単体は無防備なので、必ず`rehype-sanitize`を後段に置いて危険なタグ・属性を除去します。プラグインは配列の順に適用されるため、raw→sanitizeの順序が重要です。

```
import Markdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';

<Markdown rehypePlugins={[rehypeRaw, rehypeSanitize]}>
  {source}
</Markdown>
```

これで`<script>`や`onclick`のような危険な要素・属性は落とされ、安全なHTMLだけが残ります。

### schemaによる許可タグ・属性の定義

既定のサニタイズ設定では`target`や`class`など一部の属性が削られます。必要な属性を通すには`defaultSchema`を複製して拡張し、`rehype-sanitize`へ渡します。

```
import { defaultSchema } from 'rehype-sanitize';

const schema = {
  ...defaultSchema,
  attributes: {
    ...defaultSchema.attributes,
    a: [...(defaultSchema.attributes.a ?? []), 'target', 'rel'],
  },
};

<Markdown rehypePlugins={[rehypeRaw, [rehypeSanitize, schema]]}>
  {source}
</Markdown>
```

ホワイトリスト方式なので、明示的に許可した要素・属性以外は通りません。信頼できない入力ほど許可範囲を最小限に保つのが安全です。

## HTMLの許可・制限手段の使い分け

react-markdownはHTMLの扱いを複数の手段で制御できます。入力の信頼度に応じて次のように選び分けます。信頼できない入力にはサニタイズか無視を、信頼できる管理者入力にだけ生HTMLを許可します。

| 手段                             | 挙動           | 使う場面           |
| ------------------------------ | ------------ | -------------- |
| `skipHtml`                     | 生HTMLを無視     | 最も安全・HTML不要なとき |
| `disallowedElements`           | 指定タグを除去      | 数タグだけ禁止したい     |
| `allowedElements`              | 指定タグのみ許可     | ホワイトリスト運用      |
| `rehype-raw`単体                 | 生HTMLをそのまま描画 | 信頼できる入力限定・危険   |
| `rehype-raw`+`rehype-sanitize` | 無害化して描画      | ユーザー入力を安全に表示   |

`allowedElements`と`disallowedElements`は併用できません。除去せず中身だけ残すなら`unwrapDisallowed`を添えます。生HTMLを一切描画しない要件なら`skipHtml`が最も単純で安全です。

## カスタムコンポーネントによる見出し・リンク制御

### componentsプロップの基本

特定のタグを自前のReactコンポーネントへ差し替えるには`components`を使います。見出しやコードブロックにクラスや独自の挙動を与えたいときの標準手段です。

### 見出しへのID自動付与（rehype-slug）

見出しへIDを付けるのに自前のカスタムコンポーネントを書く必要はありません。`rehype-slug`を`rehypePlugins`に渡すと、各見出しにテキスト由来のIDが自動で付きます。

```
import rehypeSlug from 'rehype-slug';

<Markdown rehypePlugins={[rehypeSlug]}>{source}</Markdown>
```

付与されたIDは目次リンクやページ内アンカーの遷移先として使えます。

### 外部リンクの別タブ・rel付き制御

リンクを外部だけ別タブで開き、`rel`で参照元漏れとタブナビングを防ぐには、`a`要素を差し替えます。

```
<Markdown
  components={{
    a({ href, children }) {
      const external = href?.startsWith('http');
      return (
        <a
          href={href}
          target={external ? '_blank' : undefined}
          rel={external ? 'noopener noreferrer' : undefined}
        >
          {children}
        </a>
      );
    },
  }}
>
  {source}
</Markdown>
```

下線や強調などの装飾は、差し替えたコンポーネント側でクラスを当てて表現します。

## 見出しへジャンプする目次の実装

### 見出しIDの収集と目次生成

`rehype-slug`でIDを付けたうえで、描画後の見出しを走査すれば目次を組み立てられます。

```
// 描画後に見出しからIDと文言を集める
const nodes = document.querySelectorAll('h2[id], h3[id]');
const toc = Array.from(nodes).map((el) => ({
  id: el.id,
  text: el.textContent,
  level: el.tagName,
}));
```

集めた配列を`a href="#id"`のリンク一覧として並べれば、そのまま目次になります。

### アンカーへのスムーズ遷移

ページ内アンカー（`html ジャンプ`）の遷移をなめらかにするには、CSSの`scroll-behavior`だけで足ります。

```
html {
  scroll-behavior: smooth;
}
```

固定ヘッダーがある場合は、遷移先が隠れないよう見出しに`scroll-margin-top`を付けて調整します。

## よくある質問

### react-markdownのインストールコマンドは？

`npm install react-markdown`です。テーブルなどGFMを使うなら`npm install react-markdown remark-gfm`のように`remark-gfm`も併せて導入します。

### react-markdownでテーブルが表示されないのはなぜ？

標準Markdownはテーブルを解釈しないためです。`remark-gfm`を導入し`remarkPlugins={[remarkGfm]}`を指定すると、`table`として描画されます。

### react-markdownで生HTMLを安全に表示するには？

`rehype-raw`で生HTMLの解析を有効にし、後段に`rehype-sanitize`を置いて無害化します。信頼できない入力なら`skipHtml`でHTML自体を無視するのが最も安全です。

### classNameプロップが効かないのはなぜ？

10.0.0（2025年2月）でトップレベルの`className`が廃止されたためです。`<Markdown>`を`div`で囲むか、`components`で各要素にクラスを当ててください。

### 見出しにジャンプするアンカーを付けるには？

`rehype-slug`で見出しにIDを自動付与し、そのIDへ`#`付きリンクを張ります。遷移はCSSの`scroll-behavior: smooth`でなめらかにできます。

## 関連記事

- [XSSとCSRFの違いとは？仕組み・被害・対策を実装レベルで徹底比較](/tech/details/4109/)
- [Next.jsとNuxtの違いを比較｜React・Vueとの関係と選び方【2026年7月版】](/tech/details/2832/)
- [Next.js 16とは？ReactベースのWeb開発フレームワーク最新版の概要や主要機能を詳しく紹介](/tech/details/9596/)

---

出典: [react-markdownの使い方｜GFM表示とrehype-sanitizeで安全にMarkdownを描画する](<https://www.issoh.co.jp/tech/details/3660/>)（株式会社一創）
