6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-08-22

React+Apolloで無限スクロールを実装した時の備忘録を残しておこう。
無限スクロールそのものより、GraphQLのページネーションの実装で考えることが多い気がする。
↓実装イメージ
qiitaImage.gif

1.前提知識・条件

1-1.実装環境やライブラリ

クライアントはReact(Javascript)、サーバー側はHasuraでpostgreSQLと繋げるようにし、クライアント側のGraphQLの取り回しはApolloを使用。

"^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なカラム)を持たせる必要があるっぽい。

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

cursor-basedの中にさらにrelay-style-paginationという手法があり、ApolloだとInMemoryCacheでrelayStylePagination()を指定するといい感じにできるらしいが、IDの振り方とかで詰まったのでここではスルー。

1-4.ApolloのfetchMoreについて

fetchMoreはuseQueryフックに含まれる関数で、useQueryのクエリを引数だけ変えて実行したい時に使う。

スクロールで次のコンテンツをロードする度にfetchMoreを実行する感じ。

1-5.react-infinite-scroll-componentについて

スクロールさせたい要素をタグ直下の子要素として配置する。

↓使う時のイメージ

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は引数で適宜指定できる。

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

2-2.stateの管理やuseQueryの設定

ロードする度に12件づつ取得するイメージ

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で適宜呼び出す。

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
リック・アンド・モーティというアニメのキャラを延々とスクロールできる。

6
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?