Intersection Observer API を使ってスクロールに合わせて遅延読み込み(React)
はじめに
交差オブザーバー APIを使って下までスクロールしたら次の分のデータを読み込むというのをやってみました。
Intersection Observer API について
「Intersection Observer API」について簡単に説明すると、要素同士の交差率を監視し、その交差率に合わせて処理を実行させることができる機能です。root
オプションで指定した要素が監視する側の要素になります。observer.observe()
で指定した要素が監視対象の要素になります。threshold
オプションがこの監視要素と監視対象の要素の交差率になります。要素同士の交差率がthreshold
の交差率に達するとコールバックが実行されます。
コールバックに含まれるisIntersecting
で監視対象のスクロールの向きがわかります。表示領域から出ていくスクロールの場合はfalse、入っていくスクロールの場合はtrueになります。
サンプルについて
<LoadMore />
が監視対象で、<Display />
が監視する側の表示領域です。
refを使ってそれぞれdomを参照するようにしています。
// 前略
<div className="App">
<Display ref={frameRef}>
<Items items={currentData} />
{!hasReachedLimit && (
<LoadMore
ref={terminalRef}
visibility={hasLoadedData ? "visible" : "hidden"}
>
Loading more items ...
</LoadMore>
)}
</Display>
<Summary>
<LoadedNum>{loadedNum}</LoadedNum> / <Total>{DATA_LENGTH}</Total>
</Summary>
</div>
下にスクロールしていって<LoadMore />
が<Display />
内に入ってきたらコールバックが実行されるように設定しています。
具体的にはframeRef
で取得したdomをIntersectionObserverのオプションのrootに渡します。terminalRef
で取得したdomをobserver.observe(target);
で監視対象にしています。callbackにデータ取得の処理を渡しているので、交差率が閾値に達してコールバックが実行されると次のデータが読み込まれます。
最後に監視が重複しないようにuseEffectのreturn時にobserver.unobserve(target)
で監視を解除しています。これをやらないと挙動がおかしくなったりするので注意が必要です。
// 前略
const getData = useCallback(() => {
if (isLoading || hasReachedLimit) return;
setIsLoding(true);
fetchData(offset, LIMIT).then((result) => {
const nextData = [...currentData, ...result];
setCurrentData(nextData);
setLoadedNum(nextData.length);
setOffset(offset + LIMIT);
setIsLoding(false);
});
}, [currentData, isLoading, offset, hasReachedLimit]);
useIntersectionObserver({
frameRef,
terminalRef,
intersectionCallBack: getData
});
// 後略
// 前略
export const useIntersectionObserver = (args: Args) => {
const { frameRef, terminalRef, intersectionCallBack } = args;
useEffect(() => {
if (!frameRef.current || !terminalRef.current) return;
const options = {
root: frameRef.current,
rootMargin: "0px",
threshold: 0.1
};
const callback = (entries: IntersectionObserverEntry[]) => {
if (entries[0].isIntersecting) intersectionCallBack();
};
const observer = new IntersectionObserver(callback, options);
const target = terminalRef.current;
observer.observe(target);
return () => {
observer.unobserve(target);
};
}, [frameRef, terminalRef, intersectionCallBack]);
};
最後に
scrollイベントを使ったやり方に比べてパフォーマンスも良く、スクロール量の計算など煩雑な処理もせずに済むのでとても便利なAPIだと思います。スクロールに合わせて何かやるというのはUI実装の要件としては時々出てくるものですし、なるべくこういうところでライブラリも使いたくないので知っておくと重宝しそうだなと思いました。
Discussion