🤔

React Routerのnavigate stateにJSXを渡してDataCloneErrorになった話

に公開
2026/01/28

はじめに

React Router の navigate を使って画面遷移する際、うっかり state に JSX を渡してしまい、ブラウザで DataCloneError が発生してしまいました。

原因を調べていくと、ブラウザの History API が持つ「構造化クローン(Structured Clone)」 という仕組みが関係していることがわかりました。

この記事では、このエラーを題材に、

  • 何が起きていたのか
  • なぜ state に JSX を渡すとエラーになるのか
  • どのように修正したか
  • 本来どう設計すべきだったか

を整理します。同じエラーを踏んだ方の助けになれば幸いです。

エラーの状況

具体的には、以下のように navigate の state オプションに直接 JSX (React要素) を渡していました。

navigate("/error", { state: { message: <span>権限がありません</span> } })

このコードが実行されると、ブラウザのコンソールにこんなエラーが表示されます。

DataCloneError: Failed to execute 'replaceState' on 'History': ...

これは、React Router が内部で利用している History API (pushState / replaceState) が state の値を履歴に保存しようとした際に失敗したことを示しています。

なぜJSXを渡すとエラーになるのか?

結論から言うと、History API が state を保存する仕組みである「構造化クローン」が JSX (ReactElement) に対応していないからです。

構造化クローンは、JavaScriptの値を複製するためのアルゴリズムですが、複製できる型には制限があります。

✅ 構造化クローンが可能な型

  • 文字列、数値、boolean
  • Object, Array (ただし、プロパティに関数や Symbol を含まない)
  • null, Date, RegExp, Map, Set など

一方で、JSX、つまり ReactElement は、内部に Symbol や循環参照を含む特殊なオブジェクトです。そのため、構造化クローンの対象外となり、複製に失敗して DataCloneError が発生します。

navigate の state に渡した値は、最終的にブラウザの履歴に保存されるため、この制約を守る必要があったのです。

どう修正したか

修正方針はシンプルで、state には構造化クローンが可能なデータだけを渡すようにします。

1. JSX ではなく「文字列」を渡す

まず、state に渡していた JSX をただの文字列に変更しました。

frontend/src/pages/Login.tsx
const noPermissionMessage =
  "権限がありません。\n管理者へお問い合わせください。";

navigate("/error", {
  state: { message: noPermissionMessage },
  replace: true,
});

2. エラー画面のコンポーネントも string 前提に修正

遷移先のエラー画面では、location.state からメッセージを受け取ります。このとき、message の型を React.ReactNode ではなく string に限定することで、意図せず JSX が渡されてしまうのを防ぎます。

frontend/src/pages/Error.tsx
import { useLocation } from "react-router-dom";

interface LocationState {
  message?: string;
}

export const ErrorPage = () => {
  const location = useLocation();
  const { message } = (location.state as LocationState) || {};
  const errorMessage = message || "エラーが発生しました。";

  // ...
};

3. 改行文字 \n を画面に反映させる

\n を使って改行を表現したので、CSS の white-space プロパティを使って、画面上で改行が反映されるようにします。

<h1 className="... whitespace-pre-line">
  {errorMessage}
</h1>

これで、文字列として渡されたメッセージを意図通りに表示できました。

本来どう設計すべきだったか

今回のエラーによる学びは対症療法的に判明したものでしたが、より本質的な話をすると、state でUIそのものを渡すべきではない、という設計原則に行き着きます。

state は、あくまでも シリアライズ可能な「データ」 を画面間で引き継ぐためのものです。UIコンポーネントそのものではなく、UIを再現するための情報(例えばエラーコードやメッセージ文字列)を渡すのが適切です。

  • ❌ state: { message: <span>...</span> }
  • ✅ state: { message: "..." }
  • ✅ state: { errorCode: "NO_PERMISSION" }

後者のように「意味」を渡せば、遷移先のコンポーネントがそのデータに基づいてUIを構築するため、疎結合で再利用性の高い設計になります。

おまけ:よくあるNGパターン

以下のようなパターンも、関数やコンポーネント自体を渡しているため、同様に DataCloneError を引き起こします。

// ❌ コンポーネントを渡す
state: { component: <ErrorMessage /> }

// ❌ レンダリング関数を渡す
state: { render: () => <ErrorMessage /> }

どちらも構造化クローンできないため、履歴の保存に失敗します。

まとめ

  • React Router の navigate の state は、内部でブラウザの History API に渡される。
  • History API は、受け取った state を「構造化クローン」で複製して履歴に保存する。
  • JSX (ReactElement) は、構造化クローンができないため DataCloneError が発生する。
  • state には、UIそのものではなく、文字列や数値などシリアライズ可能な「データ」を渡すのが正しい設計。
  • 文字列で改行を表現したい場合は、\n と CSS の white-space: pre-line を組み合わせるのが便利。

Discussion