Next.js×microCMS|zustandを使用して無限スクロールで取得済みデータの再取得をしないようにする

ブログ一覧に無限スクロールを設置したけれどページを離れるたびに、一度取得済みのデータを取り直さないでほしい!

4/18/2025

⚡️無限スクロールはCSR

ユーザーのアクション(今回はスクロール)でデータ取得を行い、ページを表示させるCSR。

useStateuseEffect を使って無限スクロールを実装したけど、記事一覧ページから個別記事に移動して、また一覧ページに戻ると、すでに取得済みの11件目以降の記事が再度取得されてしまう

これはNext.jsの仕様で、ページを離れるとコンポーネントの状態(useStateなど)はリセットされるため、戻ってきた時には再初期化されてしまうことが原因。

一度無限スクロールで取得したデータを取り直さないようにするには、クライアントサイドで状態を保持する仕組みを導入する。

✅ 解決方法:zustandRecoilReduxなどの状態管理ライブラリを使う

状態管理ライブラリを使用することで、ブログ一覧ページを離れて詳細ページに行っても、戻ってきたら読み込んだブログ一覧は維持されたままにできる。

🧠 zustandの導入 & 実装ステップ

1. zustand をインストール

npm install zustand

2. ストアを作成(例:src/store/useBlogStore.js

⚠️Zustandの初期状態を設定する際の注意点:
ビルドした時、getStaticPropsですでに10件分の記事は取得されている状態。
Zustandの初期状態を設定する際、初期データをそのまま追加してしまうとZustandの初期状態を設定するuseEffectが走った際に、元あるデータ+初期データが追加されてデータが重複してしまう。

そのため、取得するデータの状態を「初期時」と「追加時」と「初期化」の3つの状態を用意する。

❌状態を分けないと…
ビルド時にデータを10件取得

useEffectでZustandの初期状態を設定(ビルド時に取得したデータをそのまま入れる)

20件の重複データが表示される

import { create } from "zustand";

const useBlogStore = create((set) => ({
  blogs: [],
  offset: 0,
  isEnd: false,
  initialized: false, 

  // 初回取得時に実行する(SSGデータセット)
  initBlogs: (initialBlogs) =>
    set({
      blogs: initialBlogs,
      offset: initialBlogs.length,
      isEnd: initialBlogs.length === 0,
      initialized: true, // ← 初期化フラグを立てる
    }),

  // 無限スクロール時に実行する(新しい記事を追加)
  addBlogs: (newBlogs) =>
    set((state) => ({
      blogs: [...state.blogs, ...newBlogs],
      offset: state.offset + newBlogs.length,
      isEnd: newBlogs.length === 0,
    })),
}));

export default useBlogStore;

3.一覧ページblogs/index.jsxuseBlogStore.jsを使う

import useBlogStore from "src/store/useBlogStore";

export default function Blog({ data, category }) {
const { blogs, offset, isEnd, initBlogs, addBlogs, initialized } = useBlogStore();
const [loading, setLoading] = useState(false);
const observerRef = useRef();

4. ストアに初期データをセット(初回のみ)

useEffect(() => {
    if (!initialized && blogs.length === 0 && data.length > 0) {
      initBlogs(data);//← このdataはビルド時にgetStaticPropsで取得したデータ
    }
  }, [data, blogs.length, initBlogs]);

5. 無限スクロールで新しく取得したデータをセット

スクロールに合わせてデータを取得するfetchMore関数。ここで新しく取得したデータをnewBlogsに入れて、状態管理化にあるaddBlogs(newBlogs)に渡す。
useBlogStore.jsにあるaddBlogsがこのデータを受け取り、mergeして状態を更新する。

const fetchMore = useCallback(async () => {
    if (loading || isEnd) return;
    setLoading(true);

    try {
      const res = await fetch(`/api/blogs?offset=${offset}&limit=10`);
      const newBlogs = await res.json();
      addBlogs(newBlogs);//← スクロールして取得したデータ
    } catch (error) {
      console.error("ブログの追加取得に失敗:", error);
    } finally {
      setLoading(false);
    }
  }, [offset, loading, isEnd, addBlogs]);

//... IntersectionObserverで無限スクロールの処理が続く

✅一覧ページから記事ページへ行き、戻るボタンで一覧に戻った際にデータを再取得しなくなった

Zustandで状態を管理することで、一度取得したデータを保持しているため、無限スクロールの処理が走らなくなった。

⭐️次の課題は戻るボタンで一覧に戻った際に、元いた位置に戻ること

無限スクロールで11件目以降の記事一覧を取得したけれども、記事を読んで戻るとスクロール位置はトップに戻ってしまう。
何かデータを探している時や、次は12件目の記事を読もうと思っていた時にTOPまで戻されてしまうのはとてもストレスを感じる。

Next.js|ブラウザバックで元のスクロール位置に戻る方法

❓ビルド時とスクロールで取得する時の処理を分ける必要があるのか?

SSGで記事一覧ページ(10件分)を作成し、クライアント側のスクロールによってクライアント側でデータ取得と分けているから複雑に感じた。

はじめからクライアントサイドで、一覧ページにアクセスした際にapiルートでデータを取得して10件表示させる。
そこからスクロール値に合わせて、CSR無限スクロールにしたらシンプルな構成になるのではと思った。

それぞれの構成とメリットデメリット

chatGPTに聞いたところ下記返答が返ってくる。

構成

メリット

デメリット

SSG + CSR無限スクロール

初速が速く、SEOにも強い

記事整合性が崩れる可能性あり(revalidate依存)

SSR + CSR無限スクロール

データ整合性がある程度保証される

表示速度やキャッシュ性は落ちる

CSR完全切替

自由で柔軟

SEOが効きづらい、初速が遅い

CSR完全切替だとデータ取得時に時間がかかりストレスがかかるのかもしれない…。
データの量や画像があるなしでどう変化があり、メリットデメリットがどこまで響くのかが実際に使ってみないと分からないと感じた。