Next.js で「もっと見る(無限スクロール)」したコンテンツをブラウザバック時に保持していたい
よくある「もっと見る」(もしくは無限スクロール)で段階的にコンテンツを読み込んでいくUIですが、何も考えずに実装すると遷移して返ってきたときには初期状態に戻ってしまいます。一覧から詳細へ飛んでまた一覧に戻るようなケースでは体験が著しく悪くなってしまいます。
一覧に戻ると追加読み込みしたコンテンツが消えてしまう
この記事では Next.js App Router で遷移方法の違いを考慮しつつ、遷移後も読み込んだコンテンツを保持する方法をまとめてみました。保持されるパターンは以下の表のようになります。
ブラウザバック |
<Link> で戻る |
<a> で戻る |
|
---|---|---|---|
<Link> で遷移 |
✅ | ✅ | ❌ |
<a> で遷移 |
✅ | ❌ | ❌ |
ライブラリのバージョンは以下です。
-
next
:15.1.3
-
react
:^19.0.0
-
@tanstack/react-query
:^5.62.11
デモ用のサイトとリポジトリは以下です。
- デモ
- Firebase App Hosting: https://bf-cache-test--bf-cache.asia-east1.hosted.app/
- Vercel: https://bf-cache-test.vercel.app/
- リポジトリ: https://github.com/kiyopikko/bf-cache-test
以下が動作の様子です。
ソフトナビゲーションの場合
いわゆる SPA 的な遷移、Next.js でいうと <Link>
( next/link
) を使ったナビゲーションの場合です。サイト内のページ遷移に限定されます。
こちらはとても簡単で、 @tanstack/react-query
の useInfiniteQuery
を使うとソフトナビゲーションをしている間はキャッシュが効いて、すでに読み込んだコンテンツを表示してくれます。
ブラウザバックか<Link>で戻ると保持される
他のライブラリを検証したわけではないですが TanStack Query に限らず、同じようなデータフェッチ+キャッシュ系ライブラリなら同等の API が用意されていそうです(swrならuseSWRInfiniteとか)。
ハードナビゲーションの場合
こちらは従来の MPA 的な遷移、 <a>
タグでのナビゲーションの場合です。外部サイトへの遷移(の場合は新しいタブで開きそうなものですが...)、サイト内遷移だけどNext.jsとの控えめなお付き合いをしていたり諸々の理由で <Link>
タグを使えないケースが該当します。
TanStack Query はメモリにデータをキャッシュをしているだけなので、ハードにナビゲートすると消えてしまいます。
そこで使うのが BFCache というブラウザに搭載されている機能です。以下、Yahoo! JAPAN Tech Blogから引用です。
BFCache(バックフォワードキャッシュ)は、ブラウザでページを遷移した時に、ページの完全なスナップショットとして保存されるメモリキャッシュを指します。通常遷移する場合はページが0から読み込み直されるに対し、BFCacheが有効の場合はページ全体のスナップショットから復元され、JavaScriptもそこから再開されます。
アプリ側の実装
ページ取得した後の状態を BFCache で復元させる方法は以下の記事で紹介されています。
History API の replaceState
でページ取得後の状態を履歴として登録するだけです。
// 追加読み込み
const fetchProjects = async ({ pageParam }: { pageParam: number }) => {
const res = await fetch("/api/projects?cursor=" + pageParam);
+ window.history.replaceState({}, "", window.location.toString());
return res.json();
};
こうすることで、ブラウザバックした際に BFCache が働いてページ取得後の状態を復元してくれるようになります。
外部サイトへ遷移しても復元される
注意点
実装はかなり楽なのですが注意点があります。
何らかの要因で BFCache が無効になることがある
以下、先ほども紹介した Yahoo! JAPAN Tech Blog の記事の抜粋です。
これまでブラウザに実装されてきたWeb標準技術の互換性や、セキュリティの問題によって、特定のケースで有効化できない場合が存在します。以下は、ChromiumのソースコードからBFCacheが無効化される要因を抜き出したものですが、ゆうに50以上もの無効化されるケースが存在します。
50以上...その時々で無効になるケースが多々あるという前提に立って、「有効だったら良い体験届けられてラッキー、無効になってもまったく見れなくなるわけじゃないからまあいっか」 といったプログレッシブ・エンハンスメント的な姿勢が求められそうです。
無効になっている場合、その原因を Chrome Dev Tools で調べることができます。詳しいデバッグ方法などは先ほどのヤフーの記事か以下の web.dev の記事を参照ください。
Cache-Control から no-store を外す必要がある
上記の条件のうちコントロール可能なもののひとつに Cache-Control に no-store を含めない があります。
Next.js App Router だと振る舞いがダイナミックなページの場合は no-store
がついてきます。Route Segment Config などで指定している分には変更すれば良いのですが、以下のように searchParams
などの Dynamic APIs を参照すると自動的にダイナミックになる仕様 は注意が必要です。
const Page = async (props: { searchParams: SearchParams }) => {
const searchParams = await props.searchParams;
...
}
対策としては next.config.ts
で Cache-Control を書き換えることができる(v14.2.10以降、それ以前は middleware で対応する必要があります)ので、no-store
を除いたかたちに書き換えます。
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/with-search",
headers: [
{
key: "Cache-Control",
value: "private, no-cache, max-age=0, must-revalidate",
// 元々は private, no-cache, no-store, max-age=0, must-revalidate
},
],
},
];
},
};
next dev
では使えない(動作確認できない)
next dev
は強制的に no-store
がついてきて動作確認ができないので next build && next start
する必要があります。
ソフトナビゲーションには使えない
「実装楽だし、ソフトナビゲーションも BFCache でいいや」と思う人もいるかもしれませんが、残念ながら使えないようです。
bfcache はブラウザ管理のナビゲーションに対応しているため、シングルページ アプリ(SPA)内の「ソフト ナビゲーション」には対応していません。ただし、アプリを最初から完全に再初期化するのではなく、SPA に戻る場合は、bfcache が役に立ちます。
BFCache を使うために(そこの遷移だけでも)ハードナビゲーションに切り替えることもできるかと思いますが、アナリティクスなどのセッションの取り扱い云々や体験の違和感など鑑みると、潔く TanStack Query を使ったほうが良さげかと個人的には思います。
さいごに
「もっと見る」したコンテンツをブラウザバックでも保持する方法という割とフォーカスを絞った内容について書きました。しかし、これだけで
- TanStack Query のキャッシュ機構
- BFCache
- Cache-Control
とキャッシュに関する登場人物が次々と出てきます。それぞれ今回のブラウザバックに限らず、取り扱う上での注意点が山ほどあるので、単体の機能だけに目を向けず、全体を見て取り入れるものを慎重に選ばないとなあとしみじみ思いました🍵
Discussion