🥝
SWRとSupabaseを使ったNext.jsでのページネーション
この記事について
タイトルにある通りですが、SWR+Supabaseでのページネーションを実装していきます。
SWR
今回は、useSWR
とuseSWRInfinite
を使用した2パターンをご用意してます。
useSWRInfinite
は、ページネーション用にSWRの公式が用意した専用APIのことで、
通常のページネーションでも使用できるけど、無限ローディングを実装したい方にはとくにおすすめです。
Supabase
Supabaseの方では、range()
を使用しています。
第一引数にはじまりの行、第二引数にさいごの行を指定して使います。
range(1,5)
で1~5行目、range(6,10)
で6~10行目、range(11,15)
で11~15行目といった感じ。
実装
まずは完成したページと、コードを載せておきます。
useSWR
を使用したパターン
src/pages/index.tsx
import { supabase } from "src/libs/supabase";
import { useState } from "react";
import useSWR from "swr";
import Image from "next/image";
// useSWRの第一引数のリストを受け取る
// Supabaseに用意されているrange()を使用する
// range(start, end)は、はじまりの行数と終わりの行数を指定できる
// これを使って、1~3行目,4~6行目,7~9行目とページごとに取得する行番号を変える
const fetcher = (key: string, page: number, limit: number) => {
const start = limit * (page - 1);
const end = start + limit - 1;
const { data } = await supabase.from(key).select("*").range(start, end);
return data;
};
const Index = () => {
const limit = 3; // ページ毎に取得するデータ数
const [page, setPage] = useState(1); // 現在のページ番号
// useSWRの第一引数をリスト型に。リスト全体で1つのキャッシュキーとされる。
// 第一引数をリスト型にすると、fetcherに順番に代入される。
const { data: customers, error } = useSWR(["customers", page, limit], fetcher);
return (
<div className="fixed top-0 bottom-0 inset-0 bg-gray-200 px-20 flex items-center justify-center">
<div>
<div className="p-20 rounded-t-md bg-white space-y-4">
{customers.map((customer) => {
return (
<div
key={customer.id}
className="bg-white px-4 border-2 rounded-md"
>
<div className="grid grid-cols-[auto,1fr,auto,50px] gap-3 py-3 ">
<div className="flex items-center justify-center">
{customer.pictureurl ? (
<Image
width={50}
height={50}
src={customer.pictureurl}
alt={""}
objectFit="cover"
className="flex items-center rounded-full"
/>
) : (
<div className="h-12 w-12 animate-pulse rounded-full bg-gray-200"></div>
)}
</div>
<div className="flex flex-col items-start gap-3">
<div className="flex gap-2">
<p className="text-lg font-semibold">
{customer.name}
<span className="text-sm">様</span>
</p>
</div>
</div>
</div>
</div>
);
})}
</div>
<div className="flex w-full flex-col bg-white rounded-b-md border-t">
<div>
<div className="p-4 flex items-center justify-between">
<p>{`${(page - 1) * limit + 1} - ${page * limit} / ${total}`}</p>
<div className="flex gap-1">
<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>
</div>
</div>
);
};
export default Index;
useSWRInfinite
を使用したパターン
src/pages/index.tsx
import { supabase } from "app/libs/supabase";
import { Database } from "app/types/supabase";
import { useState } from "react";
import useSWRInfinite from "swr/infinite";
import Image from "next/image";
type Customer = Database["public"]["Tables"]["customers"]["Row"];
const fetcher = async (key: string, page: number, limit: number) => {
const start = limit * page;
const end = start + limit - 1;
const { data } = await supabase.from(key).select("*").range(start, end);
return data as Customer[];
};
export const Page = () => {
const [limit, setLimit] = useState(2);
const getKey = (pageIndex: number, previousPageData: Customer[]) => {
if (previousPageData && !previousPageData.length) return null; // 最後に到達した
return ["customers", pageIndex, limit]; // SWR キー
};
const { data: customers, size, setSize } = useSWRInfinite(getKey, fetcher);
return (
<div className="fixed top-0 bottom-0 inset-0 bg-gray-200 px-20 flex items-center justify-center">
<div>
<div className="p-20 rounded-t-md bg-white space-y-4 h-[800px] w-[400px]">
{customers?.map((items) => {
return items.map((customer) => {
return (
<div
key={customer.lineid}
className="bg-white px-4 border-2 rounded-md"
>
<div className="grid grid-cols-[auto,1fr,auto,50px] gap-3 py-3 ">
<div className="flex items-center justify-center">
{customer.pictureurl ? (
<Image
width={50}
height={50}
src={customer.pictureurl}
alt={""}
objectFit="cover"
className="flex items-center rounded-full"
/>
) : (
<div className="h-12 w-12 animate-pulse rounded-full bg-gray-200"></div>
)}
</div>
<div className="flex flex-col items-start gap-3">
<div className="flex gap-2">
<p className="text-lg font-semibold">
{customer.displayname}{" "}
<span className="text-sm">様</span>
</p>
</div>
</div>
</div>
</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>
</div>
</div>
</div>
</div>
);
};
export default Page;
useSWRInfinite
のポイント
getKey()
でキャッシュキーを動的に取得
getKey()
は、名前の通りSWRのキャッシュキーを返す関数のことで、ページ番号(初期値:0)と一つ前に取得したデータ(Article[]など)を引数に取ります。
一つ前に取得したデータは、Firestore
などのカーソル式ページネーションで活用できます。
戻り値は、通常のuseSWR
のキャッシュキー(第一引数)同様、fetcher
の引数に代入して使用できます。
また、「次のデータを取得するか?」 という判断もgetKey()
の中で行ないます。
if (previousPageData && !previousPageData.length) return nullの部分
「さらに読み込む」がたったの1行
<button onClick={() => setSize(size + 1)}>さらに読み込む</button>
fetchしてきたデータの型
データは、複数のAPIレスポンスの配列になります。
公式より引用
// `data` はこのようになります
[
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
],
[
{ name: 'John', ... },
{ name: 'Paul', ... },
{ name: 'George', ... },
...
],
...
]
さいごに
以上です。
今回、仕事でSWRを使ったページネーションを実装する機会がありましたので、議事録ついでに執筆しました。
これからも、こういった技術ブログ的な記事を増やしていこうと思います。
Discussion