M

React Queryの内部

Jan 27, 2023

これは、Dominik Dorfmeister 氏のブログ記事であるInside React Queryを日本語訳してみたものです。

誤訳などあればIssueや PR を頂けると幸いです。


React Query が内部的にどのように機能しているのか、最近よく聞かれるようになりました。再レンダリングするタイミングををどうやって検知しているのか? 重複をどうやって排除するのか? どうやってフレームワークに依存しないようになっているのか?

いずれもとても良い質問です。では、私たちの愛する非同期状態管理ライブラリを細かく確認して、useQueryを呼んだら何が起きるのかを調査しましょう。

構造を理解するためには最初から始めなければいけません。

QueryClient

QueryClient with defaultOptions holds the QueryCache

全ての始まりはQueryClientです。これは、恐らくアプリケーション起動時に、インスタンスを生成するクラスで、QueryClientProviderを通してどこでも利用可能になるものです。

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

// ⬇️ クライアントを生成
const queryClient = new QueryClient();

function App() {
  return (
    // ⬇️ クライアントを配布
    <QueryClientProvider client={queryClient}>
      <RestOfYourApp />
    </QueryClientProvider>
  );
}

QueryClientProviderReact Context を利用してアプリケーション全体にQueryClientを配布します。 クライアント自体は安定した値で、1 度だけ作られる(うっかり幾度も再生成しないように注意してください)ので、Context を利用するのに最適なケースです。 アプリケーションを再レンダリングせずに、クライアントにuseQueryClientを通してアクセスできるようにするだけです。

キャッシュを格納する容器

あまり知られてないことだと思いますが、QueryClient自体はあまり多くのことを行いません。QueryCacheMutationCacheのコンテナーであり、new QueryClientを生成するとき自動的に生成されます。

また、全てのクエリやミューテーションに設定できるいくつかの初期値を持っていて、キャッシュを操作するための便利なメソッドを提供します。ほとんどのケースにおいては、キャッシュと直接やりとりすることはありません。 やりとりする場合にQueryClientを通してアクセスします。

QueryCache

さて、クライアントでキャッシュを扱えるようになりました。キャッシュとは何でしょうか?

QueryCache consists of Queries and syncs with Persisters

簡単に言うと、 QueryCache は、queryKeys をしっかりとシリアライズしたもの(queryKeyHash )によるキーと、 Queryクラスのインスタンスによる値とのインメモリーのオブジェクトです。

デフォルトの状態の React Query がインメモリーだけに保存して、それ以外のどこにも保存しないということを理解するのが、私は重要だと思います。ブラウザのページを再読み込みしたら、キャッシュはなくなります。 localstorage のような外部ストレージにキャッシュを書き込みたいときは、persisterについて一読ください。

Query

Query informs Observers

キャッシュにはクエリがあり、Queryにロジックの大部分が集まります。クエリ(自身のデータや、ステータスか直近のフェッチがいつ発生したかのようなメタ情報)に関する全ての情報が含まれているだけでなく、クエリ機能の実行も担っていて、リトライやキャンセル、重複削除のロジックも持っています。

これは、実現不可能な状態に陥らないように、内部的にステートマシーンを持ちます。例えば、すでにフェッチされている中でもクエリ機能を呼ぶ必要があれば、そのフェッチを重複削除できたりします。クエリがキャンセルされたら、以前の状態に戻ります。

何よりも重要なこととして、クエリはクエリデータに対して誰が興味を持っているかを把握していて、全ての変更に関してObserversへ伝えることができます。

QueryObserver

Observer is created by useQuery and informs the component about updates

オブザーバーは、Queryとコンポーネントとの間の接着剤です。ObserveruseQueryが呼ばれたときに生成され、常に 1 つのクエリだけを購読します。useQueryqueryKeyを渡さなければならないのは、このためです。 😉

Observerが行うのはこれだけではありません。最適化の大部分がここで行われます。Observerは、コンポーネントが使用しているQueryのプロパティーがどれかを知っているので、無関係の変更を通知する必要がありません。 例として、dataフィールドだけを使っているのであれば、バックグラウンドで再度フェッチが行われてisFetchingに変更があっても、コンポーネントの再レンダリングは不要です。

さらにObserver毎にselectorオプションを持ち、dataフィールドのどこに関心があるかを決定できます。  以前、#2: React Query Data Transformationsでこの最適化について書きました。 stealTimeの類やインターバルでのフェッチのようなタイマーのほとんどもまたオブザーバー層で発生します。

活性なクエリと非活性なクエリ

Observerを持たないQueryは、非活性なクエリと呼ばれます。どのコンポーネントからも使われていませんが、キャッシュにはまだあります。React Query DevTools を見るとき、グレイアウトした非活性のクエリを目にすることがあるでしょう。 左の数値はクエリをサブスクライブしているObserverの数を示します。

Devtools where one query has 2 observers and another one has 0

全体像

Full overview of the architecture

まとめると、ほとんどのロジックがフレームワークに依存しない Query Core の部分にあります: QueryClientQueryCacheQueryQueryObserverなどが全てここにあります。

そのため、新しいフレームワークに対するアダプターをつくることは非常に簡単です。基本的に必要となるのは、Observerを生成して、購読して、Obseverから通知があればコンポーネントを再レンダリングする方法です。 reactsoliduseQueryのアダプターは、いずれも 100 行程度のコードです。

コンポーネントの視点から

最後に、異なる視点から流れを見ましょう。コンポーネントから始まる流れです。

flow from component mounting over fetching data to rendering it

  • コンポーネントがマウントされたらuseQueryが呼ばれてObserverが生成されます。
  • ObserverQueryCacheにあるQueryを購読します。
  • サブスクリプションは、(まだ無ければ)Queryの生成のトリガーとなるか、データが古いと判断されたときにバックグラウンドで再度フェッチするトリガーとなります。
  • フェッチが開始されるとQueryの状態が変わるので、Observerはその変化を検知します。
  • Observerはいくつかの最適化を行い、新しい状態をレンダリングする更新をコンポーネントに通知します。
  • Queryの実行が完了したら、Observerにそれを知らせます。

これは考えられるさまざまな流れの中の1 つにすぎないことに注意してください。理想的な状態は、コンポーネントがマウントされたとき、既にデータがキャッシュにあることです。 より詳細には、#17: Seeding the Query Cache を一読ください。

全ての流れに共通するのは、ロジックの大半が React(もしくは Solid や Vue など)の外で発生して、ステートマシーンからの全ての更新がObserverにまで伝わって、Observerがコンポーネントにまで伝えるべきかを決定します。