この記事ではSWRのuseSWRInfinite()
を使って無限スクロールの実装方法をサンプルコードを使ってご紹介します。
サンプルコードのデモは次の通りです。
このデモの仕様は
- データは2件づつ取得
- 一番下の「読み込み中」が表示されたのをトリガーとして次のデータを取得
- 取得したデータは追加で表示
です。
このサンプルコードの紹介と解説をします。
SWRを使った無限スクロールのサンプルコード
まずは「読み込み中」が表示されたときのトリガーとなるコードを作ります。
import { useState, useEffect } from "react";
export const useIntersection = (
ref: React.MutableRefObject<HTMLDivElement>
) => {
const [intersecting, setIntersecting] = useState(false);
useEffect(() => {
if (!ref.current) return () => {};
const observer = new IntersectionObserver(([entry]) => {
setIntersecting(entry.isIntersecting);
});
let observerRefCurrent: HTMLDivElement | null = null;
if (ref.current) {
// 監視を開始
observer.observe(ref.current);
observerRefCurrent = ref.current;
}
if (observerRefCurrent) {
return () => {
// 要素の監視を終了する
observer.unobserve(observerRefCurrent as HTMLDivElement);
};
}
return () => {};
});
return intersecting;
};
次にデータ読み込みと表示部分です。
import "./styles.css";
import React, { useEffect, useRef } from "react";
import useSWRInfinite from "swr/infinite";
import { useIntersection } from "./intersection";
import axios from "axios";
type PicReturnType = {
id: string;
author: string;
width: number;
height: number;
url: string;
download_url: string;
};
export default function App() {
// トリガーのdiv要素への参照
const ref = useRef<HTMLDivElement>(null) as React.MutableRefObject<
HTMLDivElement
>;
// トリガーが表示されているか監視
const intersection = useIntersection(ref);
const limit = 2;
// useSWRInfiniteのキーとなるパラメータ付きURLを生成
const getKey = (pageIndex: number, previousPageData: PicReturnType[]) => {
if (previousPageData && !previousPageData.length) return null;
// pageIndexは0からのため+1をしてpageIndexを1からにする
return `https://picsum.photos/v2/list?page=${pageIndex + 1}&limit=${limit}`;
};
// fetch を使用してデータを取得
const {
data: picList,
error,
isValidating,
mutate,
size,
setSize
} = useSWRInfinite(
getKey,
(url): Promise<PicReturnType[]> => fetch(url).then((r) => r.json()),
{
initialSize: 2
}
);
// axios を使用してデータを取得
// const {
// data: picList,
// error,
// isValidating,
// mutate,
// size,
// setSize
// } = useSWRInfinite(getKey, (url) => axios.get(url).then((res) => res.data), {
// initialSize: 2
// });
const isEmpty = picList?.[0]?.length === 0;
const isReachingEnd =
isEmpty || (picList && picList[picList.length - 1]?.length < limit);
// 次のデータの取得
const getPics = async () => {
setSize(size + 1);
};
useEffect(() => {
// トリガーが表示されたらデータを取得
if (intersection && !isReachingEnd) {
getPics();
}
}, [intersection, isReachingEnd]);
if (error) return "failed to load";
if (!picList) return "loading";
// 一覧表示でデータを扱いやすいように整形
const pics = picList.flat();
return (
<div className="App">
<h2>SWRを使った無限スクロール</h2>
{pics.map((pic, i) => (
<div key={i} style={{ width: "40%" }}>
<img
src={pic.download_url}
style={{ width: "100%" }}
alt={pic.author}
/>
</div>
))}
{/* 次のデータを取得するトリガー */}
<div ref={ref}>
{!isReachingEnd ? "読み込み中" : "すべて読み込みました。"}
{isEmpty ? "取得するデータはありませんでした。" : null}
</div>
</div>
);
}
サンプルコードの解説
無限スクロールを実装するときにキモとなるのがトリガーとデータの取得ですが、この記事ではSWRを使った無限スクロールが趣旨なのでSWRを使っているデータの取得部分を主に説明します。
トリガーの説明は手を加えた部分を簡単に説明します。
- トリガーの監視
useSWRInfinite()
を使って次のデータを取得- 「読み込み中」の表示をトリガーとして次のデータを取得
- 最後のページのデータであることを検知する
トリガーの監視
トリガーの監視はこちらの記事を参考にして作りました。
そのまま使うobserver.observe(ref.current);
で次のエラーが起きます。
Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'.
また、ページ遷移すると次のエラーが起きます。
The ref value 'ref.current' will likely have changed by the time this effect cleanup function runs
このエラーは監視中のトリガーが外れたときに出るエラーです。
そのため、これらのエラーが起きないようにref.currentに合わせて条件分岐して対応しています。
useSWRInfinite()を使って次のデータを取得
データの取得はサンプルコードにある次のコードで行っています。
const limit = 2;
// useSWRInfiniteのキーとなるパラメータ付きURLを生成
const getKey = (pageIndex: number, previousPageData: PicReturnType[]) => {
if (previousPageData && !previousPageData.length) return null;
// pageIndexは0からのため+1をしてpageIndexを1からにする
return `https://picsum.photos/v2/list?page=${pageIndex + 1}&limit=${limit}`;
};
// fetch を使用してデータを取得
const {
data: picList,
error,
isValidating,
mutate,
size,
setSize
} = useSWRInfinite(
getKey,
(url): Promise<PicReturnType[]> => fetch(url).then((r) => r.json()),
{
initialSize: 2
}
);
このサンプルコードではuseSWRInfinite()を使用してデータを取得しています。
useSWRInfinite()
は次のように定義されています。
import useSWRInfinite from 'swr/infinite'
// ...
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
getKey
はページが変わるので関数を使って動的にURLのパラメーターを変更する必要があります。
fetcher
はuseSWR()
と使い方は同じfetcher関数です。useSWR()
を使ったことがない方はuseSWR()
を使ったデータ取得方法を参照してください。
options
ではinitialSize
を定義しています。
サンプルコードのようにinitialSize
を2にするとはじめに2ページ分のデータを取得します。
useSWRInfinite()
を使った「もっと見る」機能も簡単に作れます。
「読み込み中」の表示をトリガーとして次のデータを取得
トリガーとなるのは以下のコードです。
{/* 次のデータを取得するトリガー */}
<div ref={ref}>
{!isReachingEnd ? "読み込み中" : "すべて読み込みました。"}
{isEmpty ? "取得するデータはありませんでした。" : null}
</div>
このdivが表示されることがトリガーとなって次のコードに示すようにuseEffect()
の中のgetPics()
が発火して次のデータを取得します。
// 次のデータの取得
const getPics = async () => {
setSize(size + 1);
};
useEffect(() => {
// トリガーが表示されたらデータを取得
if (intersection && !isReachingEnd) {
getPics();
}
}, [intersection, isReachingEnd]);
最後のページのデータであることを検知する
最後のページのデータであるかはサンプルコードの次のコードで検知しています。
const isReachingEnd =
isEmpty || (picList && picList[picList.length - 1]?.length < limit);
最後に取得したページのデータがlimit
よりも少なければ最後のページだとみなし、isReachingEnd
をtrue
にします。
isReachingEnd
がtrue
になるとトリガーの表示が切り替わり、データの取得をしなくなります。
...
useEffect(() => {
// トリガーが表示されたらデータを取得
if (intersection && !isReachingEnd) {
getPics();
}
}, [intersection, isReachingEnd]);
...
{/* 次のデータを取得するトリガー */}
<div ref={ref}>
{!isReachingEnd ? "読み込み中" : "すべて読み込みました。"}
{isEmpty ? "取得するデータはありませんでした。" : null}
</div>
...
まとめ
この記事ではSWRのuseSWRInfinite()
を使って無限スクロールの実装方法をサンプルコードを使ってご紹介しました。
サンプルコードのデモは次の通りです。
サンプルコードのように無限スクロールを実装するときにキモとなるのがトリガーとデータの取得です。
サンプルコードにあるトリガーとデータの取得でポイントとなるのは次の項目です。
- トリガーの監視
useSWRInfinite()
を使って次のデータを取得- 「読み込み中」の表示をトリガーとして次のデータを取得
- 最後のページのデータであることを検知する
この記事の参考書をまとめておきます。
コメント