React Routerのnavigate stateにJSXを渡してDataCloneErrorになった話
はじめに
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 をただの文字列に変更しました。
const noPermissionMessage =
"権限がありません。\n管理者へお問い合わせください。";
navigate("/error", {
state: { message: noPermissionMessage },
replace: true,
});
2. エラー画面のコンポーネントも string 前提に修正
遷移先のエラー画面では、location.state からメッセージを受け取ります。このとき、message の型を React.ReactNode ではなく string に限定することで、意図せず JSX が渡されてしまうのを防ぎます。
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