🔍

Intersection Observer API を使ってスクロールに合わせて遅延読み込み(React)

2022/08/20に公開

はじめに

交差オブザーバー APIを使って下までスクロールしたら次の分のデータを読み込むというのをやってみました。

Intersection Observer API について

「Intersection Observer API」について簡単に説明すると、要素同士の交差率を監視し、その交差率に合わせて処理を実行させることができる機能です。rootオプションで指定した要素が監視する側の要素になります。observer.observe()で指定した要素が監視対象の要素になります。thresholdオプションがこの監視要素と監視対象の要素の交差率になります。要素同士の交差率がthresholdの交差率に達するとコールバックが実行されます。

コールバックに含まれるisIntersectingで監視対象のスクロールの向きがわかります。表示領域から出ていくスクロールの場合はfalse、入っていくスクロールの場合はtrueになります。

isIntersecting

サンプルについて

<LoadMore />が監視対象で、<Display />が監視する側の表示領域です。
refを使ってそれぞれdomを参照するようにしています。

App.tsx
// 前略
<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)で監視を解除しています。これをやらないと挙動がおかしくなったりするので注意が必要です。

App.tsx
// 前略
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
});
// 後略
useIntersectionObserver.ts
// 前略
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実装の要件としては時々出てくるものですし、なるべくこういうところでライブラリも使いたくないので知っておくと重宝しそうだなと思いました。

参考

JSでのスクロール連動エフェクトにはIntersection Observerが便利 - ICS MEDIA

Discussion