😉

【urql・キャッシュ無効化】新規作成したデータが一覧画面に反映されない?

2024/09/11に公開

解決したい問題

「あれ!?作成したはずのデータが画面に反映されないなぁ...」
フロントエンド開発をしていてこんな思いになることはないでしょうか?
例えば、「新規作成画面でレコードを作成した後に一覧画面に遷移すると、作成したはずのレコードが一覧画面に反映されない。」そんな問題に直面することがあるかと思います。

私は直近、GraphQLクライアントであるurqlを用いて開発している際に、
「とあるレコードを初めて新規作成した後に一覧画面に遷移しても作成したデータが一覧画面に反映されない問題」 に直面しました。
具体的には、下記1→2→3と行った際に、4の問題に直面しました。

問題の再現方法

  1. 一覧画面を表示(この時点ではデータが0件)
  2. 一覧画面から新規作成画面に遷移してデータを作成
  3. データ作成後、一覧画面に遷移(もしくは作成後に一覧画面にリダイレクトされる)
  4. 問題発生🚨2で作成したはずのデータが一覧画面に反映されない🫤
    (1にて最低でも一件のレコードが存在していれば4の問題は発生しない)

今回の記事では、urqlのDocument Cachingという機能を述べた上で、上記の問題が発生した原因とその解決策について述べていきます。

urqlのDocument Cachingについて

問題の解決にはurqlのキャッシュの仕組みやキャッシュが無効化される構造を理解する必要があります。

キャッシュの仕組みについて

urqlでは、それぞれのクエリの結果をキャッシュすることで、同じGraphQLリクエストを送らないような仕組みを取り入れています。その仕組みをDocument Cachingと言います。

GraphQLのリクエストはquery stringとvariableをセットにしてキャッシュされます。
したがって、同じquery stringで異なるvariableを持つ2つのqueryを送ったとしたら、2つのキャッシュが作成されます。

  • 引用

By default, urql uses a concept called Document Caching. It will avoid sending the same requests to a GraphQL API repeatedly by caching the result of each query.

キャッシュを無効化する構造について

GraphQLでは、queryのレスポンスに__typenameというオブジェクトの型名を返す仕様を持っています。

urqlではこれを利用して、mutationにて値が変更されたときに、mutationの返り値の__typenameがmutation実行以前のquery(queryAとします)で取得された__typenameと合致する場合、queryAのキャッシュを無効化して、queryAを再実行する構造を取り入れています。

  • 引用

In GraphQL the client can request additional type information by adding the __typename field to a query's selection set. This field returns the name of the type for an object in the results, and we use it to detect commonalities and data dependencies between queries and mutations.

原因と解決策

  1. 一覧画面を表示(この時点ではデータが0件)
  2. 一覧画面から新規作成画面に遷移してデータを作成
  3. データ作成後、一覧画面に遷移(もしくは作成後に一覧画面にリダイレクトされる)
  4. 問題発生🚨2で作成したはずのデータが一覧画面に反映されない🫤
    (1にて最低でも一件のレコードが存在していれば4の問題は発生しない)

具体的には、上記の「4. 問題発生🚨2で作成したはずのデータが一覧画面に反映されない🫤 の原因と解決策」について述べていきます。

原因

「キャッシュを無効化する構造について」にて述べているように、urqlはGraphQLのレスポンスの__typenameを参照することでキャッシュを無効化する構造を採用しています。
ところが、データが0件の場合はGraphQLのレスポンスに__typenameが含まれません。

例えば、返り値の配列が1件以上になるqueryのレスポンスにはしっかり__typenameが付随しています。

query SampleQuery {
  samples(filterId: "FilterModel:1", first: 10) {
    nodes {
      id
      __typename
    }
  }
}

- レスポンス
{
  "data": {
    "samples": {
      "nodes": [
        {
          "id": "Sample:1",
          "__typename": "Sample"
        }
      ]
    }
  },
}

ですが、返り値の配列が空の場合は、__typenameがレスポンスには付随していません。

query SampleQuery {
  samples(filterId: "FilterModel:2", first: 10) {
    nodes {
      id
      __typename
    }
  }
}

- レスポンス
{
  "data": {
    "samples": {
      "nodes": []
    }
  },
}

したがって、空の配列を返すqueryなど、__typenameを返り値に持たないクエリはurql上ではキャッシュを無効化することができません。

これにより、「1. 一覧画面を表示(この時点ではデータが0件)」で作成したqueryが「2. 一覧画面から新規作成画面に遷移してデータを作成」でのmutation実行後に再実行されていないため、「4. 問題発生🚨2で作成したはずのデータが一覧画面に反映されない🫤」に繋がってしまっています。

  • 引用

This cache has a small trade-off! If we request a list of data, and the API returns an empty list, then the cache won't be able to see the __typename of said list and invalidate it.

解決策:queryにadditionalTypenamesを追加する

上記の問題を解決するためには、「1. 一覧画面を表示(この時点ではデータが0件)」で作成したqueryのキャッシュに_typenameの情報を追加してあげる必要があります。

方法

queryのcontextにadditionalTypenamesを追加します。

具体的には、queryの呼び出し元にて、
以下のコード例のTodoの箇所に__typenameに追加したいオブジェクトの型名を追加してあげることでqueryのキャッシュ情報に__typenameを含めることができます。

const context = useMemo(() => ({ additionalTypenames: ['Todo'] }), []);
const [result] = useQuery({ query, context });

これにより、「2. 一覧画面から新規作成画面に遷移してデータを作成」でのmutation実行後に、一覧画面で行われた一覧取得queryのキャッシュが無効化されて、再度queryが実行されます。

忘れがちなこと

urqlではqueryのキャッシュを無効化するフックとしてmutationの返り値の__typenameが参照されています。ですので、mutationの返り値を"success: true""status: ok"などだけにしていると、フックとなる__typenameの情報を渡すことができていない、と言えます。
したがって、mutationの返り値には更新したオブジェクトのIDを返すなどして、レスポンスに__typenameを含めてあげる必要があります。

まとめ

私は上記の問題があることに気付けなかったり、問題を認識しつつ対応を忘れたりしていて、何度かレビュー時やQA時に指摘いただくことがありました。ときには、アプリ利用者からのフィードバックで気づいてしまう、なんてこともありました。
urqlに限らず、キャッシュの保持の方法や無効化についての理解を深めるのはもちろん重要です。
それに加えて、キャッシュはバグの温床になりやすいので、キャッシュ周りのバグがないかをより入念にチェックする姿勢も持ち合わていこうと思いました!

株式会社モニクル

Discussion