【React】ライブラリなしで無限スクロールさせるためのCustom Hooksを作る

# はじめに
Reactでスクロールすると次のデータを読み込むcustom hooksを作成します。実装には[交差オブザーバーAPI](https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API)を利用します。

動作は下のcodesandboxで見ることができます。コードだけ見たい方はcodesandboxを見てください。

https://codesandbox.io/embed/infinityscroll-gdydju?fontsize=14&hidenavigation=1&module=%2Fsrc%2FuseInfinityScroll.ts&theme=dark

# 交差オブザーバーAPI
対象の要素がビューポート(表示されている画面の領域)または指定した要素と交差したことを検知するAPIで、Intersection Observer APIと呼ばれています。
`IntersectionObserver`のコンストラクターを利用して`IntersectionObserver`オブジェクトを作ります。
```js
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を吐き出すようなコードは以下のように書きます。
```js
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の実装は以下のようになります。
```ts
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版のドキュメント](https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development)を読むと参考になると思います。最初はReactの開発環境で`useEffect`が2回実行されて困ったらクリーンアップを見直すところから始めていくと慣れていくと思います。`useEffect`の依存配列には`scrollObserver`と`ref`を渡しています。`scrollObserver`は`page`が変わるたびに監視を再定義して欲しいので追加しています。`ref`は`current`が`null`から`HTMLElement`になった時などに発火して欲しいので追加しています。
そして、dataを返しています。このデータはrefがビューポートに触れるたびに更新されるデータとなっているのでこのhooksを使う側で`map`関数などで配置してあげれば自由に拡張されます。このhooksは説明のため最小限で作りましたが、ビューポートじゃなくて指定した要素に触れたときに発火するように拡張するなど使いやすいように変更しても良いかと思われます。

## hooksを使った無限スクロールの紹介
hooksの作成が完了したので実際にそれを使った無限スクロールの実装例を紹介します。ここでは100件ずつ数値を取得する非同期関数を読み込み続けるものを作成します。
データ取得関数ではないですが`numGenerator`ファイルを作成して`perOneHundred`という`page`を渡すとそのページの数値配列を返す非同期関数を作成して行います。この関数の解説はこの記事の本筋ではないので飛ばします。
```ts
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を利用すると以下のような無限スクロールが可能になります。

https://codesandbox.io/embed/infinityscroll-gdydju?fontsize=14&hidenavigation=1&module=%2Fsrc%2FApp.tsx&theme=dark

listの最後に交差を検証する対象の要素を置いて、それが画面に現れたら読み込むような仕組みです。hooksを少し変更してloading状態も返すようにすると読み込み中の表示を追加できるのでおすすめです。