🎿
Next.js✖️Firebase✖️useSWRInfiniteを使ったページネーション
記事について
前回に引き続き、今回はFirebase
とswr
を使ったページネーション機能を実装していきます。
ちょっと苦戦した箇所もあったので、共有します。
Firebase
FirebaseでのページネーションはFirebase側で用意されているlimit
、startAt
,startAfter
もしくはlimitToLast
、endAt
、endBefore
を使うのが基本になるかと思います。
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;
まとめ
いかがだったでしょうか?
違った点や、「ここ、こうすればもっと良くなるよ」などの意見がありましたら、コメントいただけると幸いです。
これからも、こういった技術ブログ的な記事を増やしていこうと思います。
Discussion