React + GraphQL + Apolloで無限スクロール(Offset-based)

React+Apolloで無限スクロールを実装した時の備忘録を残しておこう。
無限スクロールそのものより、GraphQLのページネーションの実装で考えることが多い気がする。
↓実装イメージ
![qiitaImage.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136137/c76e320c-ff19-0f90-5279-da71604594a6.gif)
# 1.前提知識・条件
## 1-1.実装環境やライブラリ
クライアントはReact(Javascript)、サーバー側はHasuraでpostgreSQLと繋げるようにし、クライアント側のGraphQLの取り回しはApolloを使用。
```"@apollo/client": "^3.3.19
"react": "^17.0.2
Hasura v1.3.3
```
無限スクロールのコンポーネントはreact-infinite-scroll-componentを使用
```"react-infinite-scroll-component": "^6.1.0"```
## 1-2.無限スクロールとは
無限スクロールは、Googleの検索結果のようなページ遷移ではなく、スクロールで次々とコンテンツを表示させるUIのこと。Twitterとかインスタみたいな感じ。
全てのコンテンツを表示するのではなく、一定量のコンテンツを表示してページ下部に到達した時(する前)に、次のコンテンツをロードする必要がある。
## 1-3.2種類のページネーションについて
一定量のコンテンツの表示(ページネーション)実装はoffset-basedとcursor-basedの2種類に大別できる。
**offset-based**
LimitとOffsetを指定して、データ取得する度にOffsetの数を増やしてデータ取得範囲を伸ばしていくイメージ。実装は容易だけどデータ量が増えるとデータを見る量が増えてパフォーマンスが悪くなるらしい。SQLのLIMITとOFFSET使う感じ。
**cursor-based**
データ取得範囲の最初と最後を指定して、データ取るイメージ。SQLで言うならWhere句でデータを絞り込む感じ。GraphQLでやる場合、取得するデータに一意かつ順番があるカラム(unique,sequentialなカラム)を持たせる必要があるっぽい。

この辺の詳しい解説は↓の記事がめっちゃ参考になる。

https://qiita.com/gipcompany/items/ffee8cf0b1522a741e12

cursor-basedの中にさらにrelay-style-paginationという手法があり、ApolloだとInMemoryCacheでrelayStylePagination()を指定するといい感じにできるらしいが、IDの振り方とかで詰まったのでここではスルー。
## 1-4.ApolloのfetchMoreについて
fetchMoreはuseQueryフックに含まれる関数で、useQueryのクエリを引数だけ変えて実行したい時に使う。

https://www.apollographql.com/docs/react/pagination/core-api/#the-fetchmore-function

スクロールで次のコンテンツをロードする度にfetchMoreを実行する感じ。
## 1-5.react-infinite-scroll-componentについて
スクロールさせたい要素を<InfiniteScroll>タグ直下の子要素として配置する。

https://www.npmjs.com/package/react-infinite-scroll-component

↓使う時のイメージ

```javascript:infiniteScroll.js
<InfiniteScroll
//表示する子要素の数(スクロールする度に増やす必要がある)
dataLength={items.length} 
//ページ下部に到達した時に実行する関数を指定
next={fetchData}
//データ取得がを続けるかどうかを指定。
//trueだとページ下部に到達した時にnextで指定した関数が実行さる。
//falseだと実行されず、endMessageが表示される。
hasMore={true}
//nextの関数結果が返ってくるまで表示するう要素を指定
loader={<h4>Loading...</h4>}
//hasMoreがfalseの時に表示する要素を指定
endMessage={<p>You have seen it all</p>}
>
//スクロールさせたい要素を列挙
{sampleData.map((v) =>
  <sampleDataChild key={v.key}/>)}
</InfiniteScroll>
```
# 2.実装
## 2-1.GraphQlのクエリ
書籍情報を取得するクエリ
limitとoffsetは引数で適宜指定できる。

```javascript:getBooks.js
const GET_BOOKS = gql`
  query getBooks(
    $limit_number: Int
    $offset_number: Int
  ) {
    books(
      limit: $limit_number
      offset: $offset_number
      order_by: { updated_at: desc }
    ) {
      title
      isbn
      author
      image_path
      price
      publish_date
    }
  }
`;
```
取得データのイメージ。~~何とも言えない本のチョイス。~~
![Screenshot 2021-08-22 at 20.40.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136137/0b6cad1e-2682-5c5f-53e8-3bbec69b79c9.png)
## 2-2.stateの管理やuseQueryの設定
ロードする度に12件づつ取得するイメージ

```javascript:BookList.js
const BookLists = React() => {
  //取得したデータを格納
  const [bookData, setBookData] = useState([]);
  //オフセット数値を格納
  const [offset, setOffset] = useState(0);
  //スクロール後のデータフェッチを続けるかの判定を格納
  const [hasMore, setHasMore] = useState(true);
  //fetchMore取得
  const { fetchMore } = useQuery(GET_BOOKS,
    onCompleted: (data) => {
      if (!data.books.length) {
        const { books } = data.books;
        setBookData(books);
      }
    },
  };
  //infiniteScrollのnextで呼ぶ関数を定義
  const getBooksData = async () => {
  //呼ぶ度にoffsetに12加算
    setOffset((prevOffset) => prevOffset + 12);
    const { data } = await fetchMore({
      variables: { limit_number: 12, offset_number: offset },
    });
    // 取得したdata件数が0の時はhasMoreをfalseにして、スクロールを終了
    data.books.length === 0 && setHasMore(!hasMore);
    setBookData((prev) => [...prev, ...data.books]);
  };
  //初回画面表示に使用
  useEffect(() => {
    getBooksData();
  }, []);

  return (
    <div>
      {bookData && bookData.length > 0 && (
        <BookContents
          data={bookData}
          nextFunc={() => getBooksData()}
          hasMore={hasMore}
        />
      )}
    </div>
```
## 2-3.InfiniteScrollのコンポーネント設定
2-2で設定した内容をInfiniteScrollで適宜呼び出す。

```javascript:InfiniteBooks.js
const InfiniteBooks = (props) => {
  return (
        <InfiniteScroll
  //InfiniteScroll直下の要素配置を変えたい時に指定
          style={{ display: 'flex', flexWrap: 'wrap' }}
          dataLength={booksData.length}
          next={() => props.nextFunc()}
          hasMore={props.hasMore}
  //material-uiのローダーを入れてる
          loader={<CircularProgress />}
          endMessage={
            <h1 style={{ textAlign: 'center' }}>
              <b>You have seen it all</b>
            </h1>
          }
        >
          {booksData &&
            booksData.map((v) => 
              <BooksChild key={v.title} props={v} />)}
        </InfiniteScroll>
  );
};
```
# 3.参考
↓参考にしたcode sandbox
リック・アンド・モーティというアニメのキャラを延々とスクロールできる。

https://codesandbox.io/s/next-graphql-infinite-scroll-z1rbd