Apollo Client × codegen でのLocalStateの使い方


この記事は GraphQL Advent Calendar 2020 19日目の記事です。
前回の記事は @policeman-kh さんの GraphQLサーバーをJavaで実装してみる でした。

Apollo ClientのLocal Stateとは

公式ドキュメント

ローカルのデータをApollo Clientに保持しておくことができる仕組みです。GraphAPIからデータを取得する際に,同時にローカルのデータも取得することができるというメリットがあります。また,Reduxなどの状態管理ライブラリの代替的な役割としても使用できます。

やりたいこと

以下のようなDBが既に存在するとします。

今回,users.idactiveUserIdとしてLocal Stateに保持する方法を考えます。

今回使用した環境

  • React
  • Apollo Client
  • GraphQL Code Generator
  • hasura

queryやmutationは既に動く状態で,Local Stateを追加することを想定します。

補足:hasuraとは

PostgreSQLからGraphQL APIサーバーを自動で構築してくれるものです。Postgreのデータベースに格納したデータに対してGraphQLのクエリが発行できるようになります。

codegenの設定

公式ドキュメント
Local Stateを利用する際,GraphQLサーバー側のschemaには存在しないフィールドやカラムをclient-side schemaに自分で追加していく流れになります。なので,まずはcodegenがclient-side schemaを認識するために,codegenの設定ファイルにschemaのパスを追加します。

codegen.js
module.exports = {
  schema: [
      "http://localhost:8080/v1/graphql",
      "src/graphql/local-schema.graphql", //追加
  ],

client-side schemaの書き方

client-side schemaに以下のように書くことでactiveUserIdというフィールドをqueryに追加することができます。

src/graphql/local-schema.graphql
extend type Query {
  activeUserId: Int!
}

これを以下のように使用することができます。

src/graphql/queries/MyArticles.graphql
query MyArticles($id: Int!) {
  activeUserId @client @export(as: "id")
  user: users_by_pk(id: $id) {
    id
    name
    articles {
      id
      body
    }
  }
}

@clientの箇所は,「キャッシュされているactiveUserIdを使用する」ということです。
@export(as: "id")によって,activeUserId$idとしてusers_by_pkの引数に渡すことができます。

その結果,useQueryを使うときにわざわざactiveUserIdを渡す必要がなくなります。

const { data } = useMyArticlesQuery();

また,

console.log(data?.activeUserId);

という形でキャッシュされたactiveUserIdを取得することもできます。

※hasuraでの注意点

hasuraでは上記のlocal-schema.graphqlの書き方ではエラーが出ます。

 AggregateError: 
        GraphQLDocumentError: Cannot query field "activeUserId" on type "quer
y_root".

extend type Queryの代わりに以下のように書くことで解決しました。

src/graphql/local-schema.graphql
type query_root {
  activeUserId: Int!
}

Local Stateの値を操作する

client.writeQueryを利用します。

    await client.writeQuery({
      query: MyArticlesDocument,
      data: { activeUserId: 1 },
    });

とすることで,MyArticles queryのacitveUserIdを1に書き換えることができます。

注意点として,今回は

  • query単位でのキャッシュ
  • フィールド単位でのキャッシュ

の両方更新されるので,他のqueryでactiveUserIdを使用している場合,そちらのキャッシュも自動的に更新されます。

(キャッシュについてはGraphQL Advent Calendar 2020 2日目で詳しく触れているのでぜひ参考にしてください。)

補足

以前はclient.writeData(またはcache.writeData)というメソッドがありました。しかし,公式にある通り,v3.0で廃止されています。

client|cache.writeData have been fully removed. client|cache.writeQuery, client|cache.writeFragment, and/or cache.modify can be used to update the cache.

PRによると,cache.writeDataを呼ぶと全てのqueryを実行し直すため,非効率的だったそうです。

まとめ

Reduxなどを使わずに,Apolloだけでremoteのデータもlocalのデータも一元管理することを目的としてLocal Stateが導入されたそうです。
(参考:https://www.apollographql.com/blog/the-future-of-state-management-dd410864cae2/)

Historically, Apollo users managed that 20% in a separate Redux or MobX store. This was a doable solution for Apollo Client 1.0, but when Apollo Client 2.0 migrated away from Redux, syncing local and remote data between two stores became trickier. We often heard from our users that they wanted to encapsulate all of their application’s state inside Apollo Client and maintain one source of truth.

今回は「activeUserIdをLocalStateにいれることでqueryを簡単に使用できるようにする」という目的で使ってみましたが,もし単に状態管理をしたいだけであれば,Apollo Client v3.0での新機能のreactive variablesを使うのが良さそうです。使ってみたらまた記事に書こうと思っています。