🥝

SWRとSupabaseを使ったNext.jsでのページネーション

2022/11/20に公開約8,000字

この記事について

タイトルにある通りですが、SWR+Supabaseでのページネーションを実装していきます。
https://swr.vercel.app/ja

https://app.supabase.com/

SWR

今回は、useSWRuseSWRInfiniteを使用した2パターンをご用意してます。

useSWRInfiniteは、ページネーション用にSWRの公式が用意した専用APIのことで、
通常のページネーションでも使用できるけど、無限ローディングを実装したい方にはとくにおすすめです。
https://swr.vercel.app/ja/docs/pagination

Supabase

Supabaseの方では、range()を使用しています。
第一引数にはじまりの行、第二引数にさいごの行を指定して使います。
range(1,5)で1~5行目、range(6,10)で6~10行目、range(11,15)で11~15行目といった感じ。

実装

まずは完成したページと、コードを載せておきます。

useSWRを使用したパターン

https://youtu.be/wZOOT2rV4tw

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を使用したパターン

https://youtu.be/iTtExhkMpU8

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を使ったページネーションを実装する機会がありましたので、議事録ついでに執筆しました。

これからも、こういった技術ブログ的な記事を増やしていこうと思います。

おまけ

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

Discussion

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