はじめに
Reactでスクロールすると次のデータを読み込むcustom hooksを作成します。実装には交差オブザーバーAPIを利用します。
動作は下のcodesandboxで見ることができます。コードだけ見たい方はcodesandboxを見てください。
交差オブザーバーAPI
対象の要素がビューポート(表示されている画面の領域)または指定した要素と交差したことを検知するAPIで、Intersection Observer APIと呼ばれています。
IntersectionObserver
のコンストラクターを利用してIntersectionObserver
オブジェクトを作ります。
const observer = new IntersectionObserver(callback[, options]);
callback
には交差したタイミングで実行される関数を渡します。このcallback関数は閾値や交差したタイミングを司るIntersectionObserverEntry
の配列のentries
とコールバックが呼び出される(つまり呼び出し元自身の)IntersectionObserver
のobserver
の二つの引数を受け取ります。
options
はIntersectionObject
をカスタマイズするためのオプションオブジェクトです。このオブジェクトは3つの組み合わせを持ちます。
root
交差されることを待つ領域となる要素とrootMargin
rootのマージン、threshold
指定した領域に対象の要素がどれだけ入ったときに発火するかの閾値(0.0~1.0)を持つことができます。
例として、target
がarea
に交差したらalertを吐き出すようなコードは以下のように書きます。
const object = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
alert('交差しました');
}
});
}, {
root: area,
});
object.observe(target);
IntersectionObserver
のメソッドとして実行したobserve
は監視対象を登録するもので、unobserve
で解除することができます。
実装
observerを使って交差したときに非同期関数を読み込むようなcustom hooksを作成します。今回作成するようなどこでも使えるような機能はcustom hooksにすることでその度使いまわせるのでおすすめです。最小限で作るのでのちに紹介する拡張など、お好みにカスタマイズして使ってください。
hooksの実装
hooksの実装は以下のようになります。
import { RefObject, useCallback, useEffect, useState } from 'react';
export const useInfinityScroll = <T>(
ref: RefObject<HTMLElement | null>,
fetch: (page: number) => Promise<T[]>,
) => {
const [data, setData] = useState<T[]>([]);
const [page, setPage] = useState(1);
const scrollObserver = useCallback(
() =>
new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
fetch(page).then((data) => {
setPage(page + 1);
setData((oldValue) => [...oldValue, ...data]);
});
}
});
}),
[page, fetch],
);
useEffect(() => {
const target = ref.current;
if (target) {
const observer = scrollObserver();
observer.observe(target);
return () => {
observer.unobserve(target);
};
}
}, [scrollObserver, ref]);
return data;
};
このhooksは監視する対象をref
としてReact.RefObject
を渡して、ページに依存するデータ取得関数をfetch
として(page: number) => Promise<T[]>
を引数として渡します。Tはデータによって異なるようにするためにジェネリクス型引数を利用してそれに合わせた型に変化させます。
まず、useState
で状態を定義します。表示するデータと表示するデータのページ数を定義しました。
次にIntersectionObserver
オブジェクトを作る関数scrollObserver
です。この関数は一旦useCallbackのことを忘れるとIntersectionObserver
のコンストラクタに交差したときにfetch
関数を呼び出して取得が完了したらsetPage
とsetData
を呼び出し状態を更新するようなcallbackを渡してIntersectionObserver
オブジェクトを作ってます。scrollObserver
は後ほど紹介するuseEffect
内で動作させさせますから、レンダリングのたび新しい関数を作り直すのではなく、page
とfetch
が変わらない限りレンダリングのたびに関数を作り直させないようにuseCallback
を利用しています。
最後にuseEffect
です。先述の通りscrollObserver
を呼び出すためのものです。まず、渡されたref
からcurrent
を取得したtarget
を定義してそれがnull
ではないか確認します。null
でなければscrollObserver
を実行して取得したIntersectionObserver
オブジェクトにtarget
を監視対象として登録します。そして、最後にマウント時に行なった副作用は可能な限りアンマウント時に解除しなければいけないのでクリーンアップ関数でIntersectionObserver
のunobserve
関数で監視を解除します。なぜそのようにしなければならないのかと感じた方はReactのbeta版のドキュメントを読むと参考になると思います。最初はReactの開発環境でuseEffect
が2回実行されて困ったらクリーンアップを見直すところから始めていくと慣れていくと思います。useEffect
の依存配列にはscrollObserver
とref
を渡しています。scrollObserver
はpage
が変わるたびに監視を再定義して欲しいので追加しています。ref
はcurrent
がnull
からHTMLElement
になった時などに発火して欲しいので追加しています。
そして、dataを返しています。このデータはrefがビューポートに触れるたびに更新されるデータとなっているのでこのhooksを使う側でmap
関数などで配置してあげれば自由に拡張されます。このhooksは説明のため最小限で作りましたが、ビューポートじゃなくて指定した要素に触れたときに発火するように拡張するなど使いやすいように変更しても良いかと思われます。
hooksを使った無限スクロールの紹介
hooksの作成が完了したので実際にそれを使った無限スクロールの実装例を紹介します。ここでは100件ずつ数値を取得する非同期関数を読み込み続けるものを作成します。
データ取得関数ではないですがnumGenerator
ファイルを作成してperOneHundred
というpage
を渡すとそのページの数値配列を返す非同期関数を作成して行います。この関数の解説はこの記事の本筋ではないので飛ばします。
export const perOneHundred = async (page: number): Promise<number[]> => {
function* range(start: number, end: number) {
for (let i = start; i < end; i++) {
yield i;
}
}
return new Promise((resolve) => {
resolve([...range((page - 1) * 100, page * 100)]);
});
};
これで先ほどのhooksを利用すると以下のような無限スクロールが可能になります。
listの最後に交差を検証する対象の要素を置いて、それが画面に現れたら読み込むような仕組みです。hooksを少し変更してloading状態も返すようにすると読み込み中の表示を追加できるのでおすすめです。