【Apollo Client】fetchMore関数でページネーションを実装する際の注意点


## 例

ここでは、以下のスキーマとクエリを用いて説明していきます。`offset`と`limit`で範囲を指定してフィードアイテムを取得するというものです。


```graphql:スキーマ
type Query {
  feed(offset: Int, limit: Int): Feed!
}

type Feed {
  items: [FeedItem!]!
  total: Int!  
}

type FeedItem {
  id: ID!
  message: String!
}
```

```js:クエリ
const FEED_QUERY = gql`
  query Feed($offset: Int, $limit: Int) {
    feed(offset: $offset, limit: $limit) {
      items {
        id
        message
      }
      total
    }
  }
`;
```

## ページネーションの実装
既に[フィールドポリシーの定義](https://www.apollographql.com/docs/react/pagination/core-api/#defining-a-field-policy)は終わっているものとして、prevボタンとnextボタンがあるページネーションを実装すると以下のようになります。

```jsx
import { useQuery } from '@apollo/client';
import { useLocation, useHistory } from "react-router-dom";

const PER_PAGE = 10;
const calcOffset = (page) => ({ offset: (page - 1) * PER_PAGE });

const FeedData = () => {
  const { loading, data, fetchMore } = useQuery(FEED_QUERY, {
    variables: {
      offset: 0,
      limit: PER_PAGE
    },
  });

  const params = new URLSearchParams(useLocation().search);
  const page = params.get('page') ?? 1;

  const history = useHistory();

  if (loading) return <Loading/>;

  return (
    <Feed
      items={data.feed.items || []}
      onClickPrev={async () => {
        if (page > 1) {
          const variables = calcOffset(page - 1);
          await fetchMore({ variables });
          params.append('page', page - 1);
          history.push(`/?${params.toString()}`);
        }
      }}
      onClickNext={async () => {
        if (page < data.feed.total / PER_PAGE) {
          const variables = calcOffset(page + 1);
          await fetchMore({ variables });
          params.append('page', page + 1);
          history.push(`/?${params.toString()}`);
        }
      }}
    />
  );
}
```

しかし、この実装には1つ問題点があります。それは、ボタンをクリックするとキャッシュの有無に関わらず毎回`fetchMore`関数が実行されてしまうという点です。デベロッパーツールのネットワークタブを見てみるとわかりますが、`fetchMore`関数はキャッシュがあってもなくても毎回サーバーにリクエストを送ります。そのため、既にデータを取得したページに戻ってきたときに、無駄な通信を行ってしまうことになります。これを防ぐためには、`fetchMore`関数を実行する前に、`readQuery`や`readFragment`関数で必要なデータがキャッシュに保存されているか確認すれば良いです。実装を修正すると、以下のようになります。

```diff_jsx
- import { useQuery } from '@apollo/client';
+ import { useQuery, useApolloClient } from '@apollo/client';
import { useLocation, useHistory } from "react-router-dom";

const PER_PAGE = 10;
const calcOffset = (page) => ({ offset: (page - 1) * PER_PAGE });

const FeedData = () => {
  const { loading, data, fetchMore } = useQuery(FEED_QUERY, {
    variables: {
      offset: 0,
      limit: PER_PAGE
    },
  });

  const params = new URLSearchParams(useLocation().search);
  const page = params.get('page') ?? 1;

+  const client = useApolloClient();
+  const shouldFetchMore = (variables) => {
+    const { feed } = client.readQuery({
+      query: FEED_QUERY,
+      variables
+    });
+    return feed.items.length === 0;
+  }

  const history = useHistory();

  if (loading) return <Loading/>;

  return (
    <Feed
      items={data.feed.items || []}
      onClickPrev={async () => {
        if (page > 1) {
          const variables = calcOffset(page - 1);
-         await fetchMore({ variables });
+         if (shouldFetchMore(variables)) {
+          await fetchMore({ variables });
+         }
          params.append('page', page - 1);
          history.push(`/?${params.toString()}`);
        }
      }}
      onClickNext={async () => {
        if (page < data.feed.total / PER_PAGE) {
          const variables = calcOffset(page + 1);
-         await fetchMore({ variables });
+         if (shouldFetchMore(variables)) {
+          await fetchMore({ variables });
+         }
          params.append('page', page + 1);
          history.push(`/?${params.toString()}`);
        }
      }}
    />
  );
}

```

## 参考

https://www.apollographql.com/docs/react/pagination/core-api/

https://www.apollographql.com/docs/react/pagination/offset-based/

https://www.apollographql.com/docs/react/caching/cache-interaction/