🙌

【Apollo Client】readQueryとwriteQueryを使って、キャッシュ操作を行う。

2022/12/30に公開

今回の記事は前回の記事の続きになります。

今回は前回のプログラムに リストのアイテム数を画面の表示して、削除のミューテーションが行われたときに、

refetchを使わないで正確なアイテム数を表示したいと思います。

https://zenn.dev/jordan23/articles/2caf4ae0ac75ac

アイテム数を表示するために、lighthouseの@paginateを使いました。
PaginatorInfoのtotalを画面に表示します。
以下詳細ドキュメント
https://lighthouse-php.com/master/api-reference/directives.html#paginate

画面

削除ミューテーションが行われたときに、キャッシュを操作してプレイヤー数を変更したいと思います。
refetchを使わない理由は前回の記事を見てください。

コード

前回記事からの代替の差分になります。

  const [deletePlayer] = useMutation(DELETE_PLAYER, {
    update(cache, { data }) {
      console.log(cache);
      const id = cache.identify(data.deletePlayer);
      cache.evict({ id });
      cache.gc();

+      const existFetchPlayers = cache.readQuery({
+        query: FETCH_PLAYERS,
+      });

+      const existFetchPlayersByDeepCopy = JSON.parse(
+        JSON.stringify(existFetchPlayers)
+      );
+      existFetchPlayersByDeepCopy.fetchPlayers.paginatorInfo.total =
+        existFetchPlayersByDeepCopy.fetchPlayers.paginatorInfo.total - 1;

+      cache.writeQuery({
+        query: FETCH_PLAYERS,
+        data: existFetchPlayersByDeepCopy,
+      });
    },
  });

同じplayerデータが、無限スクロールのキャッシュとしてマージされないように修正しました。
つまりincomingのデータが、existingに存在しない場合マージされます。

また後ほど記述するwriteQueryでplayerデータがダブってしまうため

const equal = require("deep-equal");

const client = new ApolloClient({
  uri: "http://127.0.0.1:8000/graphql",
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          fetchPlayers: {
            keyArgs: false,
            merge(existing, incoming) {
              if (!existing) return incoming;
              const data = [
                ...existing.data,
                ...incoming.data.filter((player: any) => {
+                  const isExistPlayer = existing.data.find(
+                    (existingPlayer: any) => equal(existingPlayer, player)
+                  );
+                  return !isExistPlayer === undefined ? true : false;
                }),
              ];
              const paginatorInfo = incoming.paginatorInfo;
              return {
                data,
                paginatorInfo,
              };
            },
          },
        },
      },
    },
  }),
});

readQuery

https://www.apollographql.com/docs/react/caching/cache-interaction/#readquery
キャッシュに対してクエリを叩くことができます。
ミューテーションを叩く前に、FETCH_PLAYERSクエリを叩いているので、そのときに保存したキャッシュに対してクエリを叩くことになります。

const existFetchPlayers = cache.readQuery({
        query: FETCH_PLAYERS,
      });

今回行いたいのは
fetchPlayers.paginatorInfo.totalからマイナス1したキャッシュを保存することです。

この場合writeQueryを利用して、キャッシュを書き換えたいと思います。

writeQuery

https://www.apollographql.com/docs/react/caching/cache-interaction/#writequery
writeQueryを使うことによって、キャッシュのデータを操作することできます。

const existFetchPlayersByDeepCopy = JSON.parse(
        JSON.stringify(existFetchPlayers)
      );
      existFetchPlayersByDeepCopy.fetchPlayers.paginatorInfo.total =
        existFetchPlayersByDeepCopy.fetchPlayers.paginatorInfo.total - 1;

      cache.writeQuery({
        query: FETCH_PLAYERS,
        data: existFetchPlayersByDeepCopy,
      });

readQueryで取得したオブジェクトを編集して、writeQueryのdataに指定してキャッシュを書き換えたいと思います。
まず以下の処理でreadQueryで取得したオブジェクトをdeep copyします。

const existFetchPlayersByDeepCopy = JSON.parse(
        JSON.stringify(existFetchPlayers)
      );
existFetchPlayersByDeepCopy.fetchPlayers.paginatorInfo.total - 1

を書けば良くない?と考える方もいるかもしれませんが、エラーが起こります。
原因は以下のgithubを見ればわかりますが、apolloのv3ではキャッシュは読み取り専用だからです。
https://github.com/apollographql/apollo-client/issues/5903#issuecomment-581466423

deep mergeの部分が一番の躓くポイントなのかなと感じました。
あとは、以下のようにすることで
FETCH_PLAYERSのキャッシュを、existFetchPlayersByDeepCopyを取得した際に保存されるキャッシュの形式で設定できます。

 cache.writeQuery({
        query: FETCH_PLAYERS,
        data: existFetchPlayersByDeepCopy,
      });

最後に確認でlogを仕込んで、キャッシュを変更することでuseQueryが動作しているか、
new InMemoryCache内のmerge関数内にlogを仕込んでwriteQueryしたときに動作しているか確認して終わりにします。

  const { data, fetchMore } = useQuery(FETCH_PLAYERS, {
    onCompleted({ fetchPlayers }) {
+      console.log("こんにちは");
      const Arr: Player[] = [];
      fetchPlayers.data.map(
        (player: { __typename: "Player"; id: string; name: string }) =>
          Arr.push({
            id: player.id,
            name: player.name,
          })
      );
      setPlayers(Arr);
      setHasMorePages(fetchPlayers.paginatorInfo.hasMorePages);
      setCurrentPage(fetchPlayers.paginatorInfo.currentPage);
    },
  });
const client = new ApolloClient({
  uri: "http://127.0.0.1:8000/graphql",
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          fetchPlayers: {
            keyArgs: false,
            merge(existing, incoming) {
+              console.log("hello");
              if (!existing) return incoming;
              const data = [
                ...existing.data,
                ...incoming.data.filter((player: any) => {
                  const isExistPlayer = existing.data.find(
                    (existingPlayer: any) => equal(existingPlayer, player)
                  );
                  return !isExistPlayer === undefined ? true : false;
                }),
              ];
              const paginatorInfo = incoming.paginatorInfo;
              return {
                data,
                paginatorInfo,
              };
            },
          },
        },
      },
    },
  }),
});

動作してます!

では!

Discussion