🔙

ブラウザバックで無限スクロールが振り出しに戻る問題をNext.jsの「URL付きモーダル」で解決する

に公開

はじめに

「ブラウザバックで無限スクロールが振り出しに戻る」という著名な(?)問題を、Next.js App Router の Intercepting RoutesParallel Routes を組み合わせたモーダルで、従来のデメリットを解消しながらうまく回避できました。

ブラウザバックしても無限スクロールのスクロール位置が失われていない

このDEMOアプリは実際に試せます。

Next.js フレームワーク限定のアプローチですが、この問題に取り組んでいる方や、取り組んだことのある方の参考になればと思います。

従来の問題点と回避策

問題点

無限スクロールのUIはスマートフォンとの親和性もあり、かなり普及してきました。DEMOアプリのような一覧ページと複数の詳細ページから成るアプリの一覧ページにおいて、よく利用されています。(以下、このようなページ構成の前提で議論を進めていきます)

しかし、無限スクロールとブラウザバックの相性は良くないことも知られています。一覧ページで頑張ってスクロールしたのに詳細ページから戻ったらスクロールが台無し、というアプリも散見されます。

これは、無限スクロールで追加読み込みされたコンテンツはクライアントサイドでデータ取得・保持されており、ページ遷移時にその状態が失われるためです。ブラウザはURL履歴やスクロール位置は記憶しますが、JavaScriptで動的に追加したDOMやデータは次回のページ読み込み時には(デフォルトでは)復元しません。

スマートフォンではスワイプによるブラウザバック自体は行いやすい動作であり、UI設計側としては板挟みのような状況です。

回避策

ページネーションを検討する

そもそもですが、従来的なページネーションにすれば、当然このブラウザバックの問題は起きません。

その無限スクロール、本当に必要でしょうか?
これは大きなテーマですが、下記の記事等が参考になりますので、まず再検討をオススメします。

無限スクロール:利用すべきとき、避けるべきとき

bfcacheを利用する

bfcacheはブラウザがページ全体(DOMやJavaScript実行状態等)をメモリにキャッシュし、ブラウザバック時に瞬時に復元する機能です。アプリの実装に依存しないのが大きなメリットです。既存アプリの改善の場合、有力な候補と言えそうですが、適用条件の複雑さなど課題もあるようです。

こちらも本記事のテーマではなく、私に知見もないため、下記の記事等をご覧下さい。

ブラウザの戻る/進むを高速に!ヤフーにおけるBFCache有効化に向けた取り組み

詳細をモーダルや別タブ、別ウィンドウで表示する

ページ遷移を伴わずに、一覧ページからモーダルや別タブ、別ウィンドウで詳細コンテンツを表示するという回避策も知られています。

同一アプリなのに別タブや別ウィンドウで表示することが最善策となるケースは限られている気がします。一方、モーダルでの表示に関しては適切なケースは十分あるかと思います。モーダルの「コンテキストを維持したまま特定のコンテンツに集中させる」という特性は、一覧ページの閲覧中に詳細を一時的に表示するケースに適用できると考えられるからです。

しかし、従来のモーダルはクライアントの状態のみで表示を出し分けるため、モーダルを開いてもURLは変わらず一覧ページのままであり、

  • モーダル表示時にブラウザバックすると、一覧ページではなくその前に見ていたURLまで戻ってしまう: 従来のモーダルの挙動としては自然なのですが、これがユーザーの直感に反することがあり、誤ってアプリから離脱してしまうことも多々あります
  • 詳細ページのURLの共有ができない: モーダル表示時のリンクを共有されたユーザーは一覧ページにアクセスすることになります
  • 検索エンジンにインデックスされない: 詳細ページのURLが存在しないのでインデックスはされず、詳細コンテンツからの自然流入が期待できません

等の問題があります。モーダル表示を採用する場合でも、これらデメリットに甘んじることになります。

Next.js の Intercepting routes と Parallel routes のモーダルで解決する

Next.js App Router には Intercepting RoutesParallel Routes という機能があります。公式でも紹介されているように、これらを組み合わせることでURL付きのモーダルを作成できます。

詳細ページのURLは<Link>コンポーネントの利用等によるアプリ内での遷移(ソフトナビゲーション)の場合モーダル表示され、直アクセス(ハードナビゲーション)の場合は通常のページとして表示されます。Parallel routesで複数のルートを同時に描画し、さらにIntercepting routesがナビゲーションの文脈でレンダリング対象を切り替えることで実現していると言えます。

冒頭の画像はアプリ内で遷移したのでモーダルとして詳細コンテンツが表示されていますが、詳細ページに直アクセスすると、下記のようにモーダルは表示されません。

詳細ページに直アクセスした場合の表示

これが従来の詳細コンテンツのモーダルと異なる点は以下の通りです。

  • ブラウザバックでモーダルを閉じ、さらに無限スクロールしていても一覧ページの元いた位置に戻れる: これが本記事最大のテーマです。逆に、fowardすればまたモーダルを開けるので、特にiOS端末で便利です。
  • 詳細ページのURLの共有が可能: モーダル表示時はURLは詳細ページのパスに変わるため、URLの共有やブックマークが可能です
  • 検索エンジンにインデックスされる: たくさんの詳細コンテンツのロングテールがもたらす自然流入が期待できます

DEMOアプリにおける実現方法

DEMOアプリにおいては、下記のような構成で商品一覧ページに詳細ページのモーダルを差し込んでいます。

app/
├── (content)/
│   ├── @modal/                # (a) - Parallel Routesのスロット
│   │   ├── (.)items/[id]/     # (b) - Intercepting Routesでソフトナビ時に同階層(.)のルート(f)を横取りする
│   │   │   └── page.tsx       # (c) - ソフトナビ時の詳細ページ
│   │   │── default.tsx        # (d) - パスが一致しない場合、nullを返す
│   │   └── layout.tsx         # (e) - @modal配下に適用されるモーダル用のレイアウトやスタイル
│   ├── items/[id]/page.tsx    # (f) - 直アクセス時の詳細ページ
│   ├── page.tsx               # (g) - 一覧ページ
│   ├── layout.tsx             # (h) - 通常に加え、@modalのルートを並列にレンダリングする(下記参照)
│   ├── ...
├── ...

https://github.com/derarion/nextjs-infinite-scroll-app-demo/blob/main/app/(content)/layout.tsx

実際のソースはGithubで御覧ください。

DEMOアプリで実際に挙動を確認する

DEMOアプリで上記の内容を実際に確認していきます。こちらは一覧ページと複数の詳細ページから成る、一般的な構成のレスポンシブなアプリです。

アプリのユーザーが一覧ページから詳細を閲覧する通常の利用シーン

操作を再現したショート動画です。

https://www.youtube.com/shorts/WwxlTY1MYn8

  • 一覧ページにアクセスする。
  • 最下部までスクロールし、無限スクロールでコンテンツを読み込む。
  • 商品カードをタップ(クリック)する。
  • モーダルとしての詳細ページ(c)が表示される。(URLは /items/[id]形式になっている)
  • Related Productsから別の商品カードをタップ(クリック)する。
  • モーダルとしての詳細ページ(c)が表示される。(URLは /items/[id]形式になっている)
  • ブラウザバックすると前の商品のモーダルに戻る。
  • さらにブラウザバックするとモーダルが閉じ、一覧ページの元いた位置に戻る。

ポイントとしては、

  • ユーザーが「一覧ページを閲覧中である」というコンテキストを自然に認識できるように、背景が一部透過な通常のモーダルを採用しています。従来のページ遷移のようにみせるならフルモーダルを使うこともできます。((e)のlayout.tsxをそういう実装にすれば良い)
  • Parallel routesとして一覧ページと詳細モーダルは並列にレンダリングされるので、モーダル側でカートに追加すれば、一覧ページ側のカートも反応します。(状態管理はZustandを利用)
  • カテゴリータグを押下すると、<a>タグによるハードナビゲーションでカテゴリー一覧ページに遷移します。この時点でHome(/)の一覧への関心から、特定カテゴリの一覧にユーザーの関心が移ったとみなすためです。このあたりの線引きは実務上必要になるかと思います。

があります。

詳細ページのリンクを共有されてアクセスするシーン

  • /items/[id]形式のURLである詳細ページに直接アクセスする
  • (モーダルではない)正規の詳細ページ(f)が表示される。

おわりに

私が調べた限りは、現時点で無限スクロールUIとの親和性という観点でIntercepting + Parallel routesのモーダルに言及しているものがなかったので、記事にしてみました。
技術自体が発展途上であり、私自身これをproductionでまだ運用していないため、課題等も見つかると思いますが、個人的には操作感は結構良いと思います。

Next.js ユーザーはもちろん、フレームワーク選定段階でNext.js に馴染みのない方など、どなたかの参考になれば嬉しいです。

参考

DEMOアプリの技術スタックは下記です。

  • Next.js 15 (App Router)
  • React 19
  • TypeScript 5
  • Tailwind CSS 4
  • shadcn/ui (UIコンポーネントライブラリ)
  • Lucide React (アイコンライブラリ)
  • Zustand (カートの状態管理)
  • Intersection Observer API (無限スクロール実装)

ほぼClaude Codeで作りました。DEMOアプリとしては動作確認していますが、本記事のテーマ外の実装については参考程度にご覧ください。
github.com/derarion/nextjs-infinite-scroll-app-demo

Discussion