【React】React QueryのuseInfiniteQueryで無限スクロールを実装

#はじめに
React Queryの`useInfiniteQuery()`で無限スクロールを実装したので記事にまとめました。

「React Queryって何？」という方は、よろしければ前回の記事もご覧ください。

https://qiita.com/suzuki0430/items/fbcd0ce8ee10f91e3d92

#useInfiniteQueryとは
`useInfiniteQuery()`はデータの一部で返される次のクエリを追跡します。
`useQuery()`とは返す`data`の中身が異なり、`pages`と`pageParams`の2つのプロパティを含んだオブジェクトとなります。
`pages`はクエリの結果を配列で返し、`pageParams`は実行されたクエリのキーを配列で返します。

上が`pages`、下が`pageParams`の結果の例となります。
![スクリーンショット 2021-12-04 11.56.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/f973feb7-c99d-9d11-21ef-54d7862ea423.png)

また、`useInfiniteQuery()`の記述例は以下のようになります。

```js
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isLoading,
    isFetching,
    isError,
    error,
  } = useInfiniteQuery(
    'sw-people',
    ({ pageParam = initialUrl }) => fetchUrl(pageParam),
    {
      getNextPageParam: (lastPage) => lastPage.next || undefined,
    }
  );
```
`pageParam`には初期値として`defaultUrl`が設定されていますが、optionである`getNextPageParam`によって更新されます。
以上の例では、最新ページのデータ`lastPage`から次の`pageParam`を`lastPage.next || undefined`として設定していますが、`allPages`というパラメータも利用することができます。
このように、`pageParam`の現在値はReact Queryによって管理されます。

また、`useInfiniteQuery()`が返すオブジェクトのプロパティには、`useQuery()`にはなかった以下のようなものが存在します。

- `fetchNextPage`: 次ページのデータのフェッチを許可する関数
- `hasNextPage`: `getNextPageParam`によって`pageParam`が更新されたら`true`を返す(undefinedのとき`false`)
- `isFetchingNextPage`: `fetchNextPage`で次ページのデータをフェッチしている間`true`を返す(loadingのフラグに使用)

これまでに登場したワードを実際のデータ取得フローあてはめると以下のようになります。

1. コンポーネントがマウントされて最初のページをフェッチする
2. `getNextPageParam`が`pageParam`を更新する
3. `hasNextPage`が`true`であれば、イベント(スクロールやボタンクリック)によって`fetchNextPage`を実行する
4. `hasNextPage`が`undefined`になるまで2→3を繰り返す

#実装例
`useInfiniteQuery()`とReact Infinite Scrollerを使って、無限スクロールを実装します。

https://www.npmjs.com/package/react-infinite-scroller

```jsx:InfinitePeople.jsx
import InfiniteScroll from 'react-infinite-scroller';
import { useInfiniteQuery } from 'react-query';
import { Person } from './Person';

const initialUrl = 'https://swapi.dev/api/people/';
const fetchUrl = async (url) => {
  const response = await fetch(url);
  return response.json();
};

export function InfinitePeople() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isLoading,
    isFetching,
    isError,
    error,
  } = useInfiniteQuery(
    'sw-people',
    ({ pageParam = initialUrl }) => fetchUrl(pageParam),
    {
      getNextPageParam: (lastPage) => lastPage.next || undefined,
    }
  );

  if (isLoading) return <div className="loading">Loading...</div>;
  if (isError) return <div>Error! {error.toString()}</div>;

  return (
    <>
      {isFetching && <div className="loading">Loading...</div>}
      <InfiniteScroll loadMore={fetchNextPage} hasMore={hasNextPage}>
        {data.pages.map((pageData) => {
          return pageData.results.map((person) => {
            return (
              <Person
                key={person.name}
                name={person.name}
                hairColor={person.hair_color}
                eyeColor={person.eye_color}
              />
            );
          });
        })}
      </InfiniteScroll>
    </>
  );
}
```
`react-infinite-scroll`から`InfiniteScroll`をインポートして、`Person`コンポーネントをラップします。
`loadMore`プロパティに`fetchNextPage`を、`hasMore`プロパティに`hasNextPage`を設定することで次ページのデータのフェッチを制御することができるようになります。

#参考資料

https://www.udemy.com/share/104FBU3@1Ea9_7HEDFo8hyhFEq8Yx0svmU5_6rtPSwUQiJKPaVCLzcxfbTtTdgLq5vPBl8bO/

https://react-query.tanstack.com/reference/useInfiniteQuery#_top