EY-Office ブログ

今どきの無限スクロールはIntersectionObserverを使うらしい

以前 React用Infinite Scroll(無限スクロール)ライブラリーを調べてみたという記事を 書きましたが、今回UIコンポーネントのテストを書いていてreact-infinite-scroller を使うコンポーネントのテストで苦労しました。 そのさいネットを検索していたところ、今どきの無限スクロールは IntersectionObserver を使うらしいということを知りました。😅

IntersectionObserverAPI Stable Diffusionが生成した画像です

react-infinite-scrollerライブラリーの問題点

react-infinite-scroller ライブラリーを使っていて大きな問題はないのですが、しいて上げると。

  • UIテストを書くのがたいへん
    • これはreact-infinite-scrollerの問題というよりReact Tesiting Libraryが使っているjsdomの問題です
  • 最新更新されてない
    • 現時点での最終更新日は2022年04月13日です
  • 内部状態を簡単にリセットできない
    • 別のリストを表示し直す場合、いったん空のリストを表示するトリッキーなコードを書いて、表示し直しています

IntersectionObserverとは?

IntersectionObservernew IntersectionObserver(callback, options)で作成されたオブザーバーを設定したHTML要素とoptions.rootで指定された要素が交差した(重なった)時に、callbackが呼び出されます。

rootのデフォルトはブラウザーのビューポートなので、rootを指定しない場合は、オブザーバーを設定したHTML要素がスクロール等で画面に表示された瞬間にcallbackが呼び出されるので、無限スクロールの次の表示データ取得・表示の起点になります。

IntersectionObserverを使ったInfinite Scrollのサンプルコード

React用Infinite Scroll(無限スクロール)ライブラリーを調べてみたと同じものをIntersectionObserverを使って書いてみました。

  • GO RESTからのデータ取得関数loadUserはreact-infinite-scroller版と同じです
  • ② IntersectionObserverインスタントの生成
  • ③ 交差時のcallback定義
    • 複数の要素にオブザーバーを定義できますが、今回は1つなのでentries[0]をチェック
  • ④ 交差を検出したら.isIntersectingはtrueを戻します
  • ⑤ 交差を検出したらAPIで取得するデータのページを次のページに更新します
    • この部分でAPIを呼び出してもよさそうですが、このcallbackはメインスレッドで動くので重い処理を書いてはいけないそうです。APIを呼び出しは⑨で行っています
  • ⑥ IntersectionObserverのオプションとしては交差(重なり具合)の閾値は1.0なので完全にオブザーバーを設定したHTML要素が表示された瞬間に、callbackが呼び出されます
import { useEffect, useRef, useState } from 'react'
import 'bulma/css/bulma.css'

type UserType = {
  id: number,
  name: string,
  email: string,
  gender: "male" | "female" | "unknown",
  status: "active" | "inactive"
};

const sleep = (sec: number) => new Promise(resolve =>
                                 setTimeout(resolve, sec * 1000));

const App = () => {
  const [users, setUsers] = useState<UserType[]>([]);
  const [hasMore, setHasMore] = useState(true);
  const [page, setPage] = useState(0);
  const observerTarget = useRef(null);

  const loadUser = async (page: number) => {               // ← ①
    const URL = `https://gorest.co.in/public/v2/users?&page=${page}`;

    await sleep(0.5)
    const response = await fetch(URL);
    const usersData: UserType[] = await response.json();
    const count = usersData.length;

    console.log(`GET ${URL}  count=${count}`);
    setUsers([...users, ...usersData]);
    setHasMore(count > 0);
  }

  useEffect(() => {
    const observer = new IntersectionObserver(    // ← ②
      entries => {                                // ← ③
        if (entries[0].isIntersecting) {          // ← ④
          if (hasMore) {
            setPage(p => p + 1);                  // ← ⑤
           }
        }
      },
      { threshold: 1.0 }                          // ← ⑥
    );

    if (observerTarget.current) {                 // ← ⑦
      observer.observe(observerTarget.current);
    }
    return () => {                                // ← ⑧
      if (observerTarget.current) { 
        observer.unobserve(observerTarget.current);
      }
    };
  }, [hasMore, observerTarget]);


  useEffect(() => {        // ← ⑨
    if (page > 0) {
      loadUser(page);
    }
  }, [page]);

  return (
    <div className="container mt-4 p-4"
         style={ {height:400, width: "50%",overflow: "auto"} }>
      <ul className="list">                      {// ← ⑩ }
      {users.map((user, ix) =>
        <li className="list-item p-2"
            style={ {borderBottom: "solid 1px #ccc"} } key={ix}>
          {user.name}
        </li>
      )}
      </ul>
      <div ref={observerTarget}>                  {// ← ⑪ }
        {hasMore &&                                // ← ⑫
         <progress key={0} className="progress is-success is-radiusless" />}
      </div>
    </div>
  );
}
export default App
  • ⑦ ⑪のスクロール起動用divタグをIntersectionObserverに設定しています
  • ⑧ このページから他の画面に遷移した際にIntersectionObserverの設定を解除するようにしています
  • ⑨ ページpageが変更された際にAPIを呼び出しを行うuseEffect
    • ただし画面初期表示時にページ0が呼び出されないようにしています
  • ⑩ 無限スクロールするリスト
  • ⑪ スクロール起動用divタグ、スクロールでこの要素が表示されるとIntersectionObserverがcallbackを呼び出します
  • ⑫ この要素内にプログレスバーを表示します。
    • ただしサーバー側にまだデータがある場合(hasMore = true)のみ表示されます

まとめ

このように特定のライブラリーを使わなくても少ないコードで無限スクロールが作れる事がわかりました。ただし、この無限スクロールはscroll イベントをベースとしたライブラリーとは違うので簡単には置き換えられない場合もあると思いますが、少ないコードで実現できるのがメリットだと思います。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ