GraphQLクライアントにurqlをおすすめしたい

2021/07/30に公開

https://formidable.com/open-source/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という正規化されたキャッシュもあるのでそちらに移行することもできます。

ドキュメントキャッシュとは

https://formidable.com/open-source/urql/docs/basics/document-caching/

ドキュメントキャッシュはQueryとそのVariablesをハッシュ化したものをキーとして、そのレスポンスをキャッシュするというものです。

hash( stringify( query ) + stringify ( variables ) )

めちゃシンプルですね。同じQueryとVariablesがリクエストされた場合はキャッシュされているのでサーバーにリクエストを送信せずキャッシュされた値を使います。
ここまですごくシンプルですが、GraphQLのキャッシュで問題になるのはMutationを実行したときにキャッシュのどこが古くなったかを判別するかです。ドキュメントキャッシュでは、Mutationで返ってきたレスポンスに含まれる __typename を見てその __typename を含むキャッシュがある場合そのQueryを再実行するというものです。一連の流れはこんな感じです

  1. 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"
      }
    }
  }
}
  1. MutationでBookの名前を変更する
mutation UpdateBookName {
  updateBookName(id: 1, name: "new book1") {
    id
    name
    __typename
  }
}
レスポンス
{
  data: {
    updateBookName: {
      id: 1,
      name: "new book1",
      __typename: "Book"
    }
  }
}
  1. 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の refetchQueriesFukuzawaBooksPage2 を書いておこう。アプリを拡張してbooksを見てるクエリが他にできた場合それも追加しないといけないかもだけどそのとき思い出せるかな?

urqlの場合

Mutation後に自動で FukuzawaBooksPage2 が実行されて便利!

こんな感じで、Apolloでキャッシュいじるの諦めてrefetchしたいとなってきたらもうそれurqlが自動でやってることじゃんという気持ちになります。

まとめ

こんな感じでApolloの正規化されたキャッシュにくらべると効率は下回りますが、キャッシュ管理の手間は大幅に減るのがurqlのドキュメントキャッシュの特徴です。やってることは __typename を使って古くなったキャッシュを判別するというもので、仕組みとしては多少大雑把ですが納得がいくものだと思っています。ApolloでMutationのあとの refetchQueries を多用してしまっているならそれを自動化したのがurqlのドキュメントキャッシュと言えると思います。

おまけ

最近TwitterでGraphQL関連のOSSを多数リリースしているThe Guildの人たちがurqlがいいよと言ってるのを見たので今回の記事を書いたのでした。

https://twitter.com/notrab/status/1420319138011131904
https://twitter.com/dotansimha/status/1420319560671059968
https://twitter.com/n1rual/status/1420683240889856001

Discussion