はじめに
フロントエンド側でGraphqlを操作するのに最近はApolloClient + Reactを使用していますが、実装してて思うのが、
・キャッシュ操作が難しい
・日本語情報が全然ない
ということです。
そこで今回は、ApolloClient + Reactのキャッシュ操作に重点を置いて説明しようと思います。
1.ApolloClientのキャッシュの仕組み
ApolloClientはGraphqlでサーバーサイドから取得したクエリをインメモリのキャッシュ(InMemoryCache)に正規化し保存する機能があります。
ApolloClientがデータを取得する時はデフォルトの設定だとまずキャッシュを参照し、
・データがキャッシュにあればキャッシュからデータを取得
↓
・なければサーバーサイドからクエリを取得し、キャッシュする
という仕組みです。Graphqlのクエリだけではなく、認証情報などの状態もキャッシュしておくことができます。
フェッチポリシーやキャッシュデータの正規化に関しては公式ブログの方が詳しく画像付きでわかりやすく説明されているのでこちらを参照することをお勧めします。
フェッチポリシーについて
ApolloCLientのキャッシュの正規化のしくみ
2.ApolloClientのキャッシュ操作
では「キャッシュを操作する必要のある状況」はいったいどういう時でしょうか?
それは**「データベースに格納されている値が追加(insert)、変更(update)、削除(delete)された時」やログイン等でローカルの状態が変更された時がほとんどだと思います。
Graphqlのミューテーションでデータベースに格納されている値が追加、変更、削除された時やログインでローカルの状態が変化した時などは、何も処理をしなければ当然ApolloCLientのキャッシュの内容は変更されません。**そのためApolloClientで適切なキャッシュ操作を行い、キャッシュを更新する必要があります。
ApolloClientはキャッシュ操作のための様々なメソッドが用意されており、これらを使いキャッシュを操作していく必要があります。
前提条件
query GET_TODOS {
id
text
}
mutation ADD_TODO(text: $text) {
id
text
}
今回例ではTodoリストの全件を取得する「getTodos」と新たにTodoリストに追加する「addTodos」を使用します。
import { useQuery, useMutation } from '@apollo/client';
const { data } = useQuery(GET_TODOS, {
//ここにキャッシュ操作のメソッドを記述する
});
const [ addTodo ] = useMutation(ADD_TODO, {
//ここにキャッシュ操作のメソッドを記述する
})
また今回はApolloClientが提供しているReact用のフックを使用してクエリ、ミューテーションを実行した後、キャッシュ操作のメソッドを記述していくものとします。
TODO:1 {id: 1, __typename:'TODO', text: 'テスト'},
TODO:2 {id: 2, __typename:'TODO', text: 'テスト'}
またApolloClientのキャッシュにはこのような形でデータが保存されていることとします。
操作方法1・refetchQueries
1つ目のキャッシュ操作方法は「refetchQueries」です。
このメソッドは指定したクエリをキャッシュではなくGraphQLサーバーにリクエストを送り、データを取得及びキャッシュします。
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{query: GET_TODOS}]
})
この例では、「ADD_TODO」ミューテーションでTODOリストにデータを追加した後、サーバーに「GET_TODOS」クエリのリクエストを送っています。
特徴
・コードの記述量が少なく済みます。後述するread/writeQuery、read/writeFragmentよりもコードの記述量が圧倒的に少ないです。
・すでにキャッシュされたデータを考慮せずに取得するため、データを余計に取得してしまう可能性があります。
例えばすでにTODOリストのデータがすでに30件キャッシュされていたとしても、refetchQueriesはそれを考慮せずもう一度サーバーからデータを取得するため、30件分余計にデータを取得してしまうことになります。
操作方法2・read/writeQuery
2つ目のキャッシュ操作方法は「read/writeQuery」です。
readQueryはApolloClientに保存されているキャッシュを返し、writeQueryはミューテーションの結果等を正規化した状態でキャッシュに書き込むことのできるメソッドです。
ApolloClient+hooksで使用する場合はupdate
関数を用いて記述していきます。
const [addTodo] = useMutation(ADD_TODO, {
//引数「cache」はApolloCLientのキャッシュ操作ができる引数
//引数「result」はミューテーションの返り値が格納されている
update(cache, result) => {
const existCache = cache.readQuery({
query: GET_TODOS,
})
if (existCache) {
cache.writeQuery({
query: GET_TODOS,
data: {
getTodos: {
...existCache,
result
}
}
})
}
}
})
例は保存されているTODOリストのキャッシュをcache.readQuery
で呼び出し、キャッシュが存在したなら(readQuery
はキャッシュが存在しない時はnull
を返します)cache.writeQuery
で既存のキャッシュにミューテーションの返り値であるresult
を既存のキャッシュに加えて保存しています。
特徴
・すべてキャッシュを使用して行うため、refetchQueriesを使用するよりも高速です。
・refetchQueriesに比べるとコードの記述量は多くなります。
操作方法3・read/writeFragment
3つ目のキャッシュ操作方法は「read/writeFragment」です。
「Fragment」と聞くとGraphqlを使っている方なら「フラグメントの更新にしか使用できないの?」と思ってしまうかもしれませんが、そういうわけではなく、read/writeFragmentを使用することでキャッシュの部分的な読み込み/書き込みが可能になります。読み込み、書き込みの際もキャッシュに保存されている形式に囚われずに読み込み/書き込みが可能になります。
const [addTodo] = useMutation(ADD_TODO, {
update(cache) => {
const existCache = cache.readFragment({
id: "TODO:2"//readFragmentを使用する場合、読み込むキャッシュのID指定は必須
fragment: gql`
fragment _ on TODO {
text
}
`
})
if (existCache) {
cache.writeFragment({
id: "TODO:2"//writeFragmentを使用する場合、書き込むキャッシュのID指定は必須
fragment: gql`
fragment _ on TODO {
text
}
`,
data: {
text: "変更"
}
})
}
}
})
例ではキャッシュの「TODO:2」をreadFragmentで読み込み、キャッシュがあればwriteFragmentでキャッシュのtext
のみを変更しています。
特徴
・ミューテーションの返り値がGraphqlのクエリの形式ではない場合(booleanとか)でも使用することができます。(read/writeQueryの場合、キャッシュから読み込む又は書き込むクエリはキャッシュに保存されている形式である必要が有ります)
3.まとめ
今回はApolloClient+Reactでのキャッシュ操作について紹介しました。
今後もApolloClient、ApolloServerの記事をどんどん書きたいと思っていますので、よろしくお願いします!