GraphQLクライアントにurqlをおすすめしたい
GraphQLのクライアントといえばApollo Clientが使われることが多いと思いますが、urqlというクライアントをおすすめする記事です。
TL;DR
- urqlのドキュメントキャッシュはApolloみたいなキャッシュ管理が要らなくて楽だからおすすめ
- ドキュメントキャッシュは、Mutationの
__typename
を見て汚れたキャッシュを捨てる仕組み - Mutationのあと
refetchQueries
やってるならそれはurqlが自動でやってることと同じ
なぜurqlをおすすめするのか
この記事ではurqlのドキュメントキャッシュというキャッシュを仕組みだけを紹介します。雑に言うと、Apolloの正規化されたキャッシュより多少効率は落ちるものの煩雑な正規化されたキャッシュの管理から解き放たれるというものです。Apolloのキャッシュの管理に疲れた人にめちゃくちゃおすすめですし、逆にGraphQLを使い始めた初期段階で正規化されたキャッシュに学習コストを割かずにある程度のキャッシュのメリットを得られるのでGraphQLクライアントのファーストチョイスとしても良いと思います。APIもApolloとさほどかわらない useQuery
といったHooksベースなので違和感なく使えます。urqlのドキュメントキャッシュから始めてキャッシュ効率が必要になった段階でApolloに移行するとか、urqlにもGraphcacheという正規化されたキャッシュもあるのでそちらに移行することもできます。
ドキュメントキャッシュとは
ドキュメントキャッシュはQueryとそのVariablesをハッシュ化したものをキーとして、そのレスポンスをキャッシュするというものです。
hash( stringify( query ) + stringify ( variables ) )
めちゃシンプルですね。同じQueryとVariablesがリクエストされた場合はキャッシュされているのでサーバーにリクエストを送信せずキャッシュされた値を使います。
ここまですごくシンプルですが、GraphQLのキャッシュで問題になるのはMutationを実行したときにキャッシュのどこが古くなったかを判別するかです。ドキュメントキャッシュでは、Mutationで返ってきたレスポンスに含まれる __typename
を見てその __typename
を含むキャッシュがある場合そのQueryを再実行するというものです。一連の流れはこんな感じです
- GetBookクエリでBookを取得し、キャッシュする
// urqlのキャッシュの中身のイメージ
{
query: `query GetBook($id: Int) {
book(id: $id) {
id
name
__typename
}
}`,
variables: {
id: 1
},
response: {
data: {
book: {
id: 1,
name: "book1",
__typename: "Book"
}
}
}
}
- MutationでBookの名前を変更する
mutation UpdateBookName {
updateBookName(id: 1, name: "new book1") {
id
name
__typename
}
}
{
data: {
updateBookName: {
id: 1,
name: "new book1",
__typename: "Book"
}
}
}
- Mutationのレスポンスから再実行するQueryを決める
-
updateBookName
のレスポンスに__typename: "Book"
がある -
GetBook
クエリのキャッシュに__typename: "Book"
が含まれる -
GetBook
クエリを再実行する
こんな感じで、Mutationで汚れたTypeを持ってるキャッシュは全部捨てて再取得するという結構思い切りのある仕組みです。 __typename
しか見ていないので、上記の例で updateBookName(id: 2, name: "new book1")
のようにidが異なるMutationで本来はキャッシュを破棄する必要がなくても __typename: "Book"
が一致しているのでキャッシュが更新されてしまいます。キャッシュ更新の効率はApolloの正規化されたキャッシュに比べると下回ると言えると思います。その一方で、Mutationのレスポンスに変化の起こる__typenameを含めておけば間違いなくキャッシュが更新されます。これはApolloでキャッシュ管理が破綻してきたときに(僕が)思う「Mutationで汚れたキャッシュを雑でもいいから破棄したい」という動作そのもので、多少の非効率はあるもののキャッシュ管理から解き放たれると思うと受け入れられる程度のデメリットかもしれません。Mutationの比重が多いアプリケーションだと再取得のコストが無視できないとか、アプリケーションの特性にもよると思います。
こんなときにurqlが便利
前述の例のような、あるリソースを更新するというタイプだとApolloでも困らないのでurqlのありがたみがわからないかもしれないのでキャッシュの更新がやっかいだなと思うパターンを書いてみます。これらのケースでApolloの writeQuery
とかでキャッシュの細かい操作をするのはめちゃめんどくてurqlでええやんってなるはず・・・
フィルターやページングしたリストのキャッシュと作成のMutation
例:福沢諭吉の本のリストの2ページ目をキャッシュに持っている状態で本を追加するMutationを実行した場合
query FukuzawaBooksPage2 {
books(author: "福沢諭吉", page: 2) {
id
title
author
created_at
__typename # "Book"
}
}
mutation AddBook {
addBook(author: "福沢諭吉", title: "new book") {
id
title
author
created_at
__typename # "Book"
}
}
Apolloの場合
追加した本はキャッシュのどこに入る・・・?とりあえずcreated_atは最新のはずなのでリストの末尾に付け足す?authorのフィルタとMutationのInputを比較すると同じ著者かはわかるからキャッシュに入れるかの判別はできる・・・けどそこまでやる?よくわからんからもうMutation終わったらQueryを再実行したいので、Apollo Clientの refetchQueries
に FukuzawaBooksPage2
を書いておこう。アプリを拡張してbooksを見てるクエリが他にできた場合それも追加しないといけないかもだけどそのとき思い出せるかな?
urqlの場合
Mutation後に自動で FukuzawaBooksPage2
が実行されて便利!
こんな感じで、Apolloでキャッシュいじるの諦めてrefetchしたいとなってきたらもうそれurqlが自動でやってることじゃんという気持ちになります。
まとめ
こんな感じでApolloの正規化されたキャッシュにくらべると効率は下回りますが、キャッシュ管理の手間は大幅に減るのがurqlのドキュメントキャッシュの特徴です。やってることは __typename
を使って古くなったキャッシュを判別するというもので、仕組みとしては多少大雑把ですが納得がいくものだと思っています。ApolloでMutationのあとの refetchQueries
を多用してしまっているならそれを自動化したのがurqlのドキュメントキャッシュと言えると思います。
おまけ
最近TwitterでGraphQL関連のOSSを多数リリースしているThe Guildの人たちがurqlがいいよと言ってるのを見たので今回の記事を書いたのでした。
Discussion