🎿

Next.js✖️Firebase✖️useSWRInfiniteを使ったページネーション

2022/11/22に公開約6,800字

記事について

前回に引き続き、今回はFirebaseswrを使ったページネーション機能を実装していきます。
ちょっと苦戦した箇所もあったので、共有します。

Firebase

FirebaseでのページネーションはFirebase側で用意されているlimitstartAt,startAfterもしくはlimitToLastendAtendBeforeを使うのが基本になるかと思います。

1ページ目
const getArticles = async () => {
  return await getDocs(query(collection(db, "articles"), limit(3)));
};
次へ進む
import { QueryDocumentSnapshot, DocumentData } from "firebase/firestore";

const getArticlesNext = async () => {
  //前回取得した最後の行のデータ
  //snapshotのまま代入する
  const previousLastData = previousSnapshot.docs[snapshot.docs.length - 1]
  
  return await getDocs(
    query(
      collection(db, "articles"),
      startAfter(previousLastData), 
      limit(3)
      )
    );
};
前へ戻る
import { QueryDocumentSnapshot, DocumentData } from "firebase/firestore";

const getArticlesBefore = async () => {
  //前回取得した最初の行のデータ
  const previousFirstData = previousSnapshot.docs[0]
  
  return await getDocs(
    query(
      collection(db, "articles"),
      endBefore(previousFirstData),
      limitToLast(3)
      )
    );
};

上記が、Firebase使用時のページネーションの基本となります。
しかし、今回はswrを使います。
違いは、コードがキレイ、前へ戻るを考慮しなくていい(SWRのキャッシュがあるため)ところです。

実装

早速、完成したページとコードをお見せします。

const getArticlesLimit = async (key, lim, nextCursor) => {
  const ref = collection(db, key);
  const q = nextCursor
    ? query(ref, startAfter(nextCursor), limit(lim))
    : query(ref, limit(lim));
  const querySnapshot = await getDocs(q);
  return querySnapshot;
};

const fetcher = async (key, pageIndex, limit, nextCucursor) => {
  // カーソル無し → 初期ページ
  // カーソル有り → 2ページ目以降
  const snapshot = nextCucursor
    ? await getArticlesLimit(key, limit, nextCucursor)
    : await getArticlesLimit(key, limit);

  const articles = snapshot.docs.map((d) => {
    return { id: d.id, document: d.data() };
  });
  // 次のカーソルを返す
  const cursor = snapshot.docs[snapshot.docs.length - 1];
  return { articles, cursor };
};

export const Page = () => {
  const [limit, setLimit] = useState(2);

  // 引数 previousPageData は fetcher の戻り値 (今回だと { articles, cursor })
  const getKey = (pageIndex, previousPageData) => {
    if (previousPageData && !previousPageData.articles.length) return null; // 最後に到達した
    if (pageIndex === 0) return ["articles", pageIndex, limit]; //最初のキー
    return ["articles", pageIndex, limit, previousPageData.cursor]; //次からのキー;
  };
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher);

  if (!data) return <div>Loading...</div>;

  return (
    <div className="min-h-screen bg-gray-200 p-20 flex justify-center">
      <div>
        <div className="p-20 rounded-t-md bg-white space-y-4 w-[500px] h-[500px]">
          {data[size - 1]?.articles.map((article) => {
            return (
              <div
                key={article.id}
                className="bg-white px-4 border-2 rounded-md"
              >
                <p className="text-lg font-semibold">
                  {article.document.title}
                </p>
              </div>
            );
          })}
        </div>
        <div className="flex w-full flex-col bg-white rounded-b-md border-t">
          <div className="p-4 flex items-center justify-center">
            <button
              className="flex items-center justify-center border-gray-200 px-4 py-2 rounded-md border hover:border-blue-400"
              onClick={() => {
                setSize(size - 1);
              }}
            >
              戻る
            </button>
            <button
              className="flex items-center justify-center border-gray-200 px-4 py-2 rounded-md border hover:border-blue-400"
              onClick={() => {
                setSize(size + 1);
              }}
            >
              次へ
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Page;

dataは、複数のAPIレスポンスの配列になります。

公式より引用
// `data` はこのようになります
[
  [
    { name: 'Alice', ... },
    { name: 'Bob', ... },
    { name: 'Cathy', ... },
    ...
  ],
  [
    { name: 'John', ... },
    { name: 'Paul', ... },
    { name: 'George', ... },
    ...
  ],
  ...
]

おまけ

Firebaseの公式Youtubeの中でも推奨されているlimitだけを使ったページネーション
1ページ目limit(3)、2ページ目limit(6)、3ページ目limit(9)のように、ページが進むごとに、limitの上限を増やしいく方法です。

メリット

圧倒的にシンプル。
余分なプロパティは一切いらず、データの更新・削除が起こっても、上から順に取得するだけなので問題ない。

デメリット

ページが進むごとに重複フェッチが発生する。
データ数100とし、各ページ25ずつ表示したい場合
1ページ目 取得数25
2ページ目 取得数50
3ページ目 取得数75
4ページ目 取得数100
4ページ分の取得数、合計250

import { useState } from "react";
import useSWR from "swr";
import { getDocs, collection, query, limit } from "firebase/firestore";
import { firestore } from "src/libs/firestore";

const fetcher = async (key: string, lim: number) => {
  const ref = collection(firestore, key);
  const q = query(ref, limit(lim));
  const snapshot = await getDocs(q);
  return snapshot.docs.map((d) => {
    return { id: d.id, document: d.data() };
  });
};

export const Page = () => {
  const [limit, setLimit] = useState(3);
  const [page, setPage] = useState(1);
  const { data } = useSWR(["articles", limit * page], fetcher);

  if (!data) return <div>Loading...</div>;

  const start = limit * (page - 1);
  const end = limit * page - 1;

  const articles = data.slice(start, end);

  return (
    <div className="min-h-screen bg-gray-200 p-20 flex justify-center">
      <div>
        <div className="p-20 rounded-t-md bg-white space-y-4 w-[500px] h-[500px]">
          {articles.map((article) => {
            return (
              <div
                key={article.id}
                className="bg-white px-4 border-2 rounded-md"
              >
                <p className="text-lg font-semibold">
                  {article.document.title}
                </p>
              </div>
            );
          })}
        </div>
        <div className="flex w-full flex-col bg-white rounded-b-md border-t">
          <div className="p-4 flex items-center justify-center">
            <button
              className="flex items-center justify-center border-gray-200 px-4 py-2 rounded-md border hover:border-blue-400"
              onClick={() => {
                setPage(page - 1);
              }}
            >
              戻る
            </button>
            <button
              className="flex items-center justify-center border-gray-200 px-4 py-2 rounded-md border hover:border-blue-400"
              onClick={() => {
                setPage(page + 1);
              }}
            >
              次へ
            </button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Page;

まとめ

いかがだったでしょうか?

違った点や、「ここ、こうすればもっと良くなるよ」などの意見がありましたら、コメントいただけると幸いです。
これからも、こういった技術ブログ的な記事を増やしていこうと思います。

おまけ

現在Next.js, React等を使った受託開発、業務委託事業を展開しています。
開発で困っているかた、手が足りない企業様などいらっしゃいましたらご連絡お待ちしております。
https://a-release.com

Discussion

ログインするとコメントできます