記事のサマリー(TL;DR)
- Next.js のクライアントルーティングでは
Refererが使えず、window.navigation.entries()で履歴を取得する - 単純な「1つ前のURL参照」では往復ナビゲーション時に戻り先が狂う。スタック構造と
resetsStackフラグの組み合わせが最終解 - 一覧画面など「コンテキストをリセットすべきページ」をドメイン知識として明示定義することで、安定した動作を実現
業務 SaaS の UI 開発者が注目すべき実装パターン
本記事は SmartHR のスキル・資格・研修機能(Next.js 製)で実際に発生した問題と解決策を示したものです。「一覧→詳細→別詳細」のような複数ステップの遷移が存在する業務 SaaS では、同様の課題が高頻度で発生します。
kintone や Salesforce の専用 UI を Rails や Next.js で構築している場合も、詳細画面の戻りリンクは「呼び出し元の画面」をコンテキストとして保持する必要があります。Navigation API(Chrome 102+ / Edge 対応)は Referer や sessionStorage を使わずにブラウザ履歴スタックを操作できるため、CSR 環境での戻りリンク実装の有力な選択肢です。ただし Firefox・Safari の対応状況は執筆時点でまだ限定的なため、採用時はブラウザサポート要件の確認が必要です。
詳細
要件
SmartHR のスキル・資格・研修機能には、詳細画面から上位画面へ戻るためのリンク(UpwardLink)が複数存在します。戻り先は以下の条件を満たす必要があります。
- 従業員一覧画面から詳細画面に遷移した場合 → 「従業員一覧へ戻る」
- スキルマトリクス画面から遷移した場合 → 「スキルマトリクスへ戻る」
- 「資格一覧 → 資格詳細 → 従業員詳細」のように複数ステップを経た場合 → 直前のページへ戻る
- Next.js(クライアントサイドルーティング)上で動作すること
実装1:Navigation API によるシンプルな実装
window.navigation.entries() で履歴を取得し、1つ前の URL を参照してページ名と URL を返す実装です。
[コードは原文をご参照ください]
問題点: 「資格一覧 → 資格詳細 → 従業員詳細」と遷移して UpwardLink で資格詳細に戻ると、資格詳細の戻り先が「資格一覧」ではなく「従業員詳細」になってしまいます。UpwardLink によるリンク遷移も1件の履歴として積まれるためです。
実装2:直前ページの URL を考慮するループ処理
1つ前の履歴(entries[i-1])が現在ページと同じなら「現在ページから遷移した先」とみなしてスキップするロジックを追加しました。
問題点: チェックは「1つ前」のみに限定されるため、往復が2回以上繰り返された履歴では「どこまで遡るべきか」を一般的に決定できません。「① → ② → ③ → ④ → ③(戻る)→ ②(戻る)→ ④(戻る?)」というケースで誤った戻り先を返します。
実装3:スタック構造の導入
window.navigation.entries() から独自スタックを構築し、往復した履歴を「1本の経路」に圧縮するアルゴリズムです。
[コードは原文をご参照ください]
この場合、スタックは [①, ②] に圧縮されます。
buildBackTargetStack のアルゴリズム:
- pathname がスタック未登録 →
push - pathname が既登録 → その pathname 以降をすべて
popしてその位置を最新 URL で置き換え
問題点: AppNavi(プロダクト内の主要画面切り替えコンポーネント)を経由して「従業員一覧 → 同じ従業員詳細」に再遷移した際、実装3は「同一 pathname の再登場 = 戻ってきた」と解釈し、従業員一覧をスタックから削除してしまいます。結果として戻り先が「従業員一覧」ではなく「資格詳細」になるバグが発生しました。
実装4:resetsStack フラグによるスタックリセット(最終実装)
一覧画面・マトリクス画面はトップレベルのページであり、そこへ遷移した時点でナビゲーションのコンテキストが切り替わります。この特性を resetsStack: true として明示し、該当ページに到達した際にスタックをリセットします。
[コードは原文をご参照ください]
buildBackTargetStack では各エントリを処理する際に3つのパスに分岐します。
| 条件 | 処理 |
|---|---|
resetsStack === true |
スタックを空にしてそのページから積み直す(processContextSwitch) |
| pathname 未登録 | スタック末尾に追加(processNewPage) |
| pathname 既登録 | それ以降を pop して最新 URL で置き換え(processBackNavigation) |
この実装により、AppNavi 経由で一覧画面を経由してから同じ詳細画面に再遷移しても、スタックが一覧画面からの新しいコンテキストとして正しくリセットされます。
結論
Navigation API 自体はシンプルな API ですが、実際のユーザー操作—往復ナビゲーション、AppNavi による文脈切り替え、別経路からの再訪問—を考慮すると、単純な「1つ前参照」では安定しません。最終的には ページのドメイン知識(一覧はコンテキストをリセットする)をコードに明示的に反映することで、任意の遷移パターンに対応できる実装になりました。
(おまけ)router.back() で実装する案について
「戻る」ならブラウザバックと同じ router.back() で実装できないか、という案もあります。ただし「従業員一覧画面へ戻る」のように 遷移元の画面名を表示したい要件がある場合、結局のところ同等の履歴解析ロジックが必要になります。リンク遷移で実装していること自体が複雑さを生んでいる根本原因ではあるものの、画面名表示の要件を外せない場合は本記事の実装が現実的な選択肢です。