immer で簡単 Apollo Client キャッシュ更新

こんにちは、CADDi でフロントエンドエンジニアをしている桐生です。

弊社では バックエンドとの通信に GraphQL を採用し、そのクライアントライブラリとして Apollo Client を使用しています。

今回は Apollo Clientimmer を使った Tips を紹介したいと思います。

[toc]

前提

  • apollo-client@2.6.4
  • immer@5.3.4

先に結論

  • assumeImmutableResults: true を設定して、パフォーマンスアップを図ろう。
  • freezeResults: true を設定して、キャッシュの mutable change を禁止しよう。
  • キャッシュ更新は immer を使って簡潔に直感的に実装しよう。

Apollo Client のキャッシュとは

Apollo Client の便利な仕組みの1つにキャッシュ機構 InMemoryCache があります。これは簡単に言えば 正規化されたデータが格納された Redux state のようなものです。state の正規化については Redux 公式ドキュメント

https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape/

にも記載されているので参考にしてください。

キャッシュの更新方法

Apollo Client のキャッシュへのアクセス方法としては

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

に記載されている4つのメソッドがあります。

  • readQuery
  • readFragment
  • writeQuery
  • writeFragment

使い方はそれぞれ以下のようになります。

1. readQuery x writeQuery を使った更新

// query の定義
const query = gql`
  query TodoList {
    todos {
      id
      text
      completed
    }
  }
`;

// キャッシュからの読み出し
const { todos } = client.readQuery({ query });

// データの更新
const newTodo = {
  id: '4',
  text: 'Todo 4',
  completed: false,
  __typename: 'Todo',
};
const newTodos = [...todos, newTodo];

// キャッシュへの書き込み
client.writeQuery({ query, data: { todos: newTodos } });

2. readFragment x writeFragment を使った更新

const id = '3';
// fragment の定義
const fragment = gql`
  fragment todo on Todo {
    id
    text
    completed
  }
`;

// キャッシュからの読み出し
const todo = client.readFragment({ id, fragment });

// データの更新
const completedTodo = { ...todo, completed: true };

// キャッシュへの書き込み
client.writeFragment({
  id,
  fragment,
  data: completedTodo
});

mutable なキャッシュ更新も可能だが、Apollo Client 3からは非推奨に

上記のデータ更新は immutable に行っていますが、実は mutable にも更新を行うことができます。厳密には readQuery/readFragment の戻り値はスナップショットなので、直接変更しても writeQuery/writeFragment を行わない限りキャッシュデータに変更が入るようなことはありません。

したがって、現状では以下のように記述することも可能です。そして、こちらの方が実装が直感的でわかりやすいのが正直なところです。

1. readQuery x writeQuery を使った更新(mutable)

...
// キャッシュからの読み出し
const { todos } = client.readQuery({ query });

// データの更新(mutable)
todos.push({
  id: '4',
  text: 'Todo 4',
  completed: false,
  __typename: 'Todo',
});

// キャッシュへの書き込み
client.writeQuery({ query, data: { todos } });

2. readFragment x writeFragment を使った更新(mutable)

...
// キャッシュからの読み出し
const todo = client.readFragment({ id, fragment });

// データの更新
todo.completed = true;

// キャッシュへの書き込み
client.writeFragment({
  id,
  fragment,
  data: todo
});

ただし https://blog.apollographql.com/whats-new-in-apollo-client-2-6-b3acf28ecad1 で言及されているように、パフォーマンス最適化のために、Apollo Client 3 からはキャッシュデータそのものを返すようになるため、上記のような mutable な変更を行うと問題になります。そこで Apollo Client 3 への布石として Apollo Client 2.6 では2つのオプションが追加されました。

  • assumeImmutableResults: アプリケーションコードがキャッシュを mutable に変更しないと確信している場合、この仮定をApolloClientコンストラクターに伝えることで、パフォーマンスの大幅な改善を実現できる。
  • freezeResults : devモードですべてのキャッシュ結果を凍結し、mutableな変更をできないようにする。

この2つのオプションは Apollo Client 3 ではデフォルトで true に設定される予定です。そのため今のうちからこれらのオプションを有効にして immutable なキャッシュ更新を強制しておくのがベターと言えます。

immer で immutable なキャッシュ更新を

ようやく immer の出番です。

つまるところ、Redux のようにキャッシュは immutable に更新すべし、ということなんですが、ただそうなると、やはり Redux と同じで spread hell という問題に悩まされることになります。

これを解消してくれるのが immer です。最近では redux-toolkit にも採用されており、immutable な更新を mutable 的に直感的に記述することが可能になっています。

1. readQuery x writeQuery を使った更新 with immer

import { produce } from 'immer';
...
// キャッシュからの読み出し
const { todos } = client.readQuery({ query });

// データの更新 with immer
const newTodos = produce(todos, draft => {
  draft.push({
    id: '4',
    text: 'Todo 4',
    completed: false,
    __typename: 'Todo',
  });
});

// キャッシュへの書き込み
client.writeQuery({ query, data: { todos: newTodos } });

2. readFragment x writeFragment を使った更新 with immer

import { produce } from 'immer';
...
// キャッシュからの読み出し
const todo = client.readFragment({ id, fragment });

// データの更新 with immer
const newTodo = produce(todo, draft => draft.completed = true);

// キャッシュへの書き込み
client.writeFragment({
  id,
  fragment,
  data: newTodo
});

この例はデータ構造が単純なので、そこまでの恩恵は得られませんが、より複雑な構造をもつデータに対しては効力を発揮するでしょう。

ぜひ immer を使ってみてください。