history-state-form-preservation-research
History.state を用いたユーザ入力値の保存 ― 調査レポート
1. History.state でユーザ入力値を保存するライブラリ・フレームワーク
1.1 フレームワーク組み込み機能
Inertia.js ― 最も直接的な実装
Inertia.js は useRemember / useForm で History.state にフォームデータを自動保存・復元 する機能を公式に備えている。
// Vue 3 例 — 自動保存
import { useRemember } from "@inertiajs/vue3";
const form = useRemember({ first_name: null, last_name: null });
// useForm にキーを渡すだけで自動保存 + バリデーションエラーも記憶
import { useForm } from "@inertiajs/vue3";
const form = useForm("CreateUser", data); // 静的キー
const form = useForm(`EditUser:${props.user.id}`, data); // 動的キー
// 手動管理
import { router } from "@inertiajs/vue3";
router.remember(data, "my-key"); // 保存
let data = router.restore("my-key"); // 復元
| 項目 | 詳細 |
|---|---|
| 保存先 | history.replaceState() — ブラウザ履歴エントリの state オブジェクト |
| 復元トリガー | popstate イベント(ブラウザの戻る/進むボタン) |
| 対応フレームワーク | Vue / React / Svelte |
| バックエンド | Laravel / Rails / 任意 |
| セキュリティ | .dontRemember() でパスワード等の除外が可能 |
| 制限事項 | ブラウザの replaceState() 呼び出し頻度制限あり。高頻度更新はデバウンス必須 |
出典:
- Inertia.js 公式ドキュメント — Remembering State: https://inertiajs.com/docs/v3/data-props/remembering-state
- Inertia.js v1 ドキュメント: https://v1.inertiajs.com/remembering-state
React Router v6+ / Remix ― スクロール復元は実装済み、フォームは提案段階
React Router: <ScrollRestoration> でスクロール位置は History.state に保存済み。フォームデータの自動復元は v7 で future.flags として導入予定(現在 GitHub Discussion で議論中)。
// 提案されている実装イメージ(React Router v7)
// location.key ごとにフォームデータをメモリ + sessionStorage に保存
// <Form> マウント時に input.defaultValue !== input.value なら復元
// <Form restore="off"> でオプトアウト可能
Remix: ScrollRestoration コンポーネントは History.state ベースで実装済み。フォームデータは useFetcher + location.state で手動管理が必要。
出典:
- React Router Discussion #9947 — "Persist Form state across locations": https://github.com/remix-run/react-router/discussions/9947
- Remix ScrollRestoration ドキュメント: https://v2.remix.run/docs/components/scroll-restoration
Turbo (Hotwire)
Turbo Drive は History API でナビゲーションを管理。turbo:before-cache でフォーム状態をリセットしたり、Stimulus コントローラで pushState をカスタム実装するパターンが一般的。フォーム自動保存は組み込みではない。
出典:
- Turbo Handbook — Drive: https://turbo.hotwired.dev/handbook/drive
- Turbo Streams Custom push_state action: https://hotwire.club/blog/2026-04-14-turbo-streams-custom-stream-actions-push-state
- Issue #894 — Restoring GET-form state on restoration visits: https://github.com/hotwired/turbo/issues/894
1.2 専用ライブラリ
| ライブラリ | 保存先 | 説明 | 出典 |
|---|---|---|---|
useHistoryState (カスタムフック) |
history.replaceState() |
React Router の useHistory を使用。useState の代わりに useHistoryState に置き換えるだけで全入力値が永続化 |
gal.hagever.com / CodeSandbox |
| react-form-autosave | localStorage |
フォーム自動保存に特化。除外フィールド・デバウンス・Undo/Redo対応。History.state は不使用 | GitHub |
| form-storage (appleple) | localStorage |
フォームデータを自動保存。History.state は不使用 | 公式サイト |
| history.js (browserstate) | History API フォールバック | HTML4 ブラウザへの pushState/replaceState ポリフィル。主にSPAナビゲーション用 |
GitHub |
1.3 セキュリティ観点での注意点
History.state にユーザ入力を保存する場合のリスク:
- XSS 時の影響: History.state は
documentスコープでアクセス可能。XSS 脆弱性がある場合、保存されたセンシティブデータが盗まれる - データ容量制限: ブラウザによって
pushState/replaceStateの state サイズ制限が異なる。Firefox: 640KB(シリアライズ済み文字数制限、再起動後もディスクに保存するため)。Chrome: 仕様上の制限なし(1MB超で動作確認済み)。IE11: 約524KB。超過時は例外スロー - ブラウザ履歴の閲覧: 開発者ツールで
history.stateは閲覧可能。機密データは保存しない - タブ間隔離: History.state はタブごとに独立。
localStorageのようなクロスタブ衝突の問題はない(利点)
2. History API の実装経緯と想定用途
2.1 背景 ― AJAX/SPA の台頭とブラウザ歴史管理の崩壊
2000年代後半、AJAX と Single-Page Application (SPA) が普及するにつれ、従来のブラウザ履歴管理では不十分な状況が生じた:
- 従来の History API:
history.length,history.back(),history.forward(),history.go()のみ。開発者が履歴エントリを追加・編集する手段はなかった - ハッシュbang (
#!) のハック: 開発者は#フラグメント(例:#!/page/2)で擬似的なルーティングを実装。Google も AJAX クローリング仕様でこれを推奨した - 問題点: URL が意味をなさなくなり、ブックマーク・共有・SEO・アクセシビリティが損なわれた
2.2 設計思想
HTML5 History API は以下の原則で設計された:
"URLs are historically important markers for resources. Make them meaningful and organised. Make sure you can directly access them without JavaScript. Only then should you add your JavaScript to enhance the browsing experience."
— HTML5 Doctor
"I don't think I can overstate the importance of the URL. URLs are permanent: users make favorites of them, search engines index them and companies market them."
— Clark Sell, Microsoft (MSDN Magazine, Aug 2012)
コアゴール:
- JavaScript アプリケーションがブラウザの戻る/進むボタンを正しく機能させる
- URL をクリーンなパス(
/page/2)に変更可能にする(#なし) - 各アプリケーション状態に対応する意味のある URL を維持する
- サーバー側でその URL を直接解釈可能にする(プログレッシブエンハンスメント)
2.3 W3C 仕様での定義
W3C HTML5 Working Draft (2011) で以下のように定義された:
- セッション履歴: ブラウジングコンテキスト内の
Documentのシーケンス - 履歴エントリ:
URLとstate object(または両方)で構成。タイトル、フォームデータ、スクロール位置などの情報も含む - state オブジェクトの用途:
- 事前解析済みURL状態(マイナーな最適化)
- URL に入れたくない一時的なドキュメント固有の状態(例: アニメーション座標、キャッシュポインタ)
出典:
- W3C HTML5 WD — Section 5.4 Session history and navigation: https://www.w3.org/TR/2011/WD-html5-20110113/history.html
2.4 ブラウザ実装の歴史
| ブラウザ | バージョン | 年 |
|---|---|---|
| Chrome | 5+ | 2010 |
| Safari | 5.0+ | 2010 |
| Firefox | 4.0+ | 2011 |
| Opera | 11.50+ | 2011 |
| iOS Safari | 4+ | 2010 |
| IE | 未サポート(polyfill 必要) | — |
ポリフィル: History.js が HTML4 の hashchange + フラグメント識別子で API をエミュレート
2.5 仕様が想定していた本来の用途 vs 現状
| 本来の設計用途 | 現状の使われ方 |
|---|---|
| SPA ルーティング(ページ遷移の履歴管理) | ✅ 主要用途。React Router, Vue Router, Angular Router 等が標準採用 |
クリーンURLの維持(# なし) |
✅ 標準的に使用 |
| スクロール位置の保存 | ✅ React Router / Remix の ScrollRestoration |
| アプリケーション状態の保存(アニメーション座標等) | ⚠️ 一部で使用 |
| フォーム入力値の保存 | ⚠️ 仕様の本来の用途ではないが、Inertia.js 等で応用されている |
仕様上、state オブジェクトは「URL に入れたくない一時的なドキュメント固有の状態」を想定していた。フォームデータ保存は明示的な用途ではないが、タブ間隔離という特性から Inertia.js のようなフレームワークが応用している。
3. なぜ Web Storage ではなく History.state を使うのか
Web Storage(localStorage / sessionStorage)でも技術的にはフォーム保存は可能だが、history.state を採用する理由は以下の通り。
3.1 タブ間隔離 (Tab Isolation)
| 保存先 | タブ間で共有? | 特徴 |
|---|---|---|
localStorage |
✅ 共有される | 同一オリジン内の全タブ・ウィンドウで共有。マルチタブで同時にフォームを開くとデータが衝突 |
sessionStorage |
❌ タブごとに隔離 | 各タブで独立。ただし window.open() で開いたサブウィンドウは親のsessionStorageを共有する |
history.state |
❌ タブごとに隔離 | 各タブは独自の履歴スタックを持つ。さらに履歴エントリごとにも分離される |
localStorage と異なり、sessionStorage と history.state はどちらもタブ隔離されている。ただし sessionStorage は同一タブ内の全ページ遷移で共有されるのに対し、history.state は履歴エントリごとに分離される点が根本的に異なる。
3.2 履歴エントリとの自動紐付け
history.state の最大の利点は、状態が履歴エントリ自体に紐付くこと:
| 保存先 | 状態と履歴の関係 | 管理コスト |
|---|---|---|
| Web Storage | URLや履歴エントリと関係なくキーバリューで保存 | どのページにどの状態が対応するかを手動で管理する必要がある(location.key 等をキーにする工夫が必要) |
history.state |
状態が履歴エントリに直接含まれる | ブラウザが「どの状態をいつ復元すべきか」を自動管理。戻る/進む時に popstate イベントで自動的に渡される |
3.3 ライフサイクル
| 保存先 | データが消えるタイミング | フォーム保存への適合性 |
|---|---|---|
localStorage |
ユーザが手動で削除するまで残り続ける | ❌ 不要なデータが永続的に残り、メモリ/容量を消費 |
sessionStorage |
タブを閉じるまで残り続ける | ⚠️ 不要なナビゲーション後も残り続ける |
history.state |
履歴エントリが消える時(履歴の最大件数に達して追い出される、またはオリジン遷移) | ✅ 必要な期間だけ保持して自動で消える |
3.4 まとめ
| 観点 | localStorage |
sessionStorage |
history.state |
|---|---|---|---|
| タブ間衝突 | あり | なし(タブ隔離) | なし(タブ隔離) |
| 履歴エントリごとの分離 | なし | なし(同一タブ内で共有) | ✅ あり |
| 戻る/進むとの連携 | 手動実装が必要 | 手動実装が必要 | 自動(popstate で復元) |
| キー管理 | 手動 | 手動(location.key 等をキーに) |
不要(履歴エントリに自動紐付) |
| データ寿命 | 永続的 | タブ閉じるまで | 履歴エントリと同期して自動削除 |
Inertia.js の useRemember が history.replaceState() を採用しているのは、履歴エントリごとの自動分離と戻る/進むとの自動連携による。
4. セキュリティ考察 — History.state に妥当な代替手段は存在するか
4.1 根本的なトレードオフ
history.state の3つの利点(タブ隔離・履歴エントリごとの状態・即時復元)は、全て「JavaScript からアクセス可能なクライアントサイド記憶領域」であることに依存している:
| 利点 | なぜクライアントサイドが必要か |
|---|---|
| タブ間隔離 | サーバーサイドはブラウザのタブを識別できない |
| 履歴エントリごとの状態 | ブラウザの履歴スタックはクライアント側のみが管理 |
| 即時復元(popstate) | サーバー往復では復元にレイテンシが発生しUXが損なわれる |
そして クライアントサイドの記憶領域は全て XSS に対して等しく脆弱である。history.state も sessionStorage も localStorage も、XSS が成立すれば攻撃者に読み取られる点は変わらない。
4.2 代替手段の検討
| 保存先 | タブ隔離 | 履歴エントリごとの自動分離 | XSS耐性 |
|---|---|---|---|
history.state |
✅ | ✅ | ❌ |
sessionStorage |
✅ | ❌(同一タブ内の全ページで共有) | ❌ |
localStorage |
❌ | ❌ | ❌ |
| HttpOnly Cookie | ✅ | ❌ | ✅(JSからアクセス不可) |
| サーバーサイドセッション | ✅(SessionID必要) | ✅(リクエスト毎に状態を管理) | ✅ |
sessionStorage はタブ隔離されているが、履歴エントリごとには分離されない。同一タブ内の全ページ遷移で共有されるため、「戻る」で前のページのフォーム状態を復元するには location.key 等をキーにした手動管理が必要。つまり history.state の「履歴エントリとの自動紐付け」を維持できるクライアントサイドの代替手段は 存在しない。
サーバーサイドセッションや HttpOnly Cookie は XSS 耐性があるが、「即時復元」の UX とサーバー負荷のトレードオフが発生する。
4.3 結論
history.stateの利点(タブ隔離 + 履歴自動紐付け)を享受しつつ、XSSリスクを排除する手段はない
取れる対策は根本的な代替ではなく、防御の多層化に限られる:
- 機密データを保存しない — Inertia.js の
.dontRemember()がまさにこれ。パスワード、トークン、PII は除外する - CSP (Content Security Policy) で XSS 自体の発生確率を下げる
- 入力値のサニタイズ — state に保存前にエスケープ
- サーバーサイドのフォーム復元 — 完全な別アプローチ(UX とサーバー負荷のトレードオフ)
まとめ: 「フォームデータを History.state に保存すべきではない」という認識は正しい。それをやるなら「機密データは絶対に含めない」が必須のラインとなる。Inertia.js 等がこれを採用しているのは、UX のメリットがセキュリティリスクを上回ると判断しているからであり、リスクがゼロになったわけではない。
2.6 主要リファレンス
- MDN — History API: https://developer.mozilla.org/en-US/docs/Web/API/History_API
- MDN — Working with the History API: https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API
- MDN — pushState(): https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
- MDN — replaceState(): https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
- MDN — popstate event: https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event
- HTML5 Doctor — Pushing and Popping with the History API: http://html5doctor.com/history-api
- MSDN Magazine — Building HTML5 Applications: A History (API) Lesson: https://learn.microsoft.com/en-us/archive/msdn-magazine/2012/august/building-html5-applications-a-history-api-lesson
- CSS-Tricks — Using the HTML5 History API: https://css-tricks.com/using-the-html5-history-api
- SitePoint — How to Modify the Browser History: https://www.sitepoint.com/javascript-history-pushstate
- Treehouse Blog — Getting Started With The History API: https://blog.teamtreehouse.com/getting-started-with-the-history-api
- W3C HTML5 WD — History: https://www.w3.org/TR/2011/WD-html5-20110113/history.html