こんにちは、CADDi でフロントエンドエンジニアをしている桐生です。
弊社では バックエンドとの通信に GraphQL
を採用し、そのクライアントライブラリとして Apollo Client
を使用しています。
今回は Apollo Client
と immer
を使った 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 を使ってみてください。