React Queryの内部
Jan 27, 2023
これは、Dominik Dorfmeister 氏のブログ記事であるInside React Queryを日本語訳してみたものです。
誤訳などあればIssueや PR を頂けると幸いです。
React Query が内部的にどのように機能しているのか、最近よく聞かれるようになりました。再レンダリングするタイミングををどうやって検知しているのか? 重複をどうやって排除するのか? どうやってフレームワークに依存しないようになっているのか?
いずれもとても良い質問です。では、私たちの愛する非同期状態管理ライブラリを細かく確認して、useQuery
を呼んだら何が起きるのかを調査しましょう。
構造を理解するためには最初から始めなければいけません。
QueryClient
全ての始まりはQueryClient
です。これは、恐らくアプリケーション起動時に、インスタンスを生成するクラスで、QueryClientProvider
を通してどこでも利用可能になるものです。
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// ⬇️ クライアントを生成
const queryClient = new QueryClient();
function App() {
return (
// ⬇️ クライアントを配布
<QueryClientProvider client={queryClient}>
<RestOfYourApp />
</QueryClientProvider>
);
}
QueryClientProvider
はReact Context を利用してアプリケーション全体にQueryClient
を配布します。
クライアント自体は安定した値で、1 度だけ作られる(うっかり幾度も再生成しないように注意してください)ので、Context を利用するのに最適なケースです。
アプリケーションを再レンダリングせずに、クライアントにuseQueryClient
を通してアクセスできるようにするだけです。
キャッシュを格納する容器
あまり知られてないことだと思いますが、QueryClient
自体はあまり多くのことを行いません。QueryCache
やMutationCache
のコンテナーであり、new QueryClient
を生成するとき自動的に生成されます。
また、全てのクエリやミューテーションに設定できるいくつかの初期値を持っていて、キャッシュを操作するための便利なメソッドを提供します。ほとんどのケースにおいては、キャッシュと直接やりとりすることはありません。
やりとりする場合にQueryClient
を通してアクセスします。
QueryCache
さて、クライアントでキャッシュを扱えるようになりました。キャッシュとは何でしょうか?
簡単に言うと、 QueryCache
は、queryKeys をしっかりとシリアライズしたもの(queryKeyHash )によるキーと、 Query
クラスのインスタンスによる値とのインメモリーのオブジェクトです。
デフォルトの状態の React Query がインメモリーだけに保存して、それ以外のどこにも保存しないということを理解するのが、私は重要だと思います。ブラウザのページを再読み込みしたら、キャッシュはなくなります。 localstorage のような外部ストレージにキャッシュを書き込みたいときは、persisterについて一読ください。
Query
キャッシュにはクエリがあり、Query
にロジックの大部分が集まります。クエリ(自身のデータや、ステータスか直近のフェッチがいつ発生したかのようなメタ情報)に関する全ての情報が含まれているだけでなく、クエリ機能の実行も担っていて、リトライやキャンセル、重複削除のロジックも持っています。
これは、実現不可能な状態に陥らないように、内部的にステートマシーンを持ちます。例えば、すでにフェッチされている中でもクエリ機能を呼ぶ必要があれば、そのフェッチを重複削除できたりします。クエリがキャンセルされたら、以前の状態に戻ります。
何よりも重要なこととして、クエリはクエリデータに対して誰が興味を持っているかを把握していて、全ての変更に関してObservers
へ伝えることができます。
QueryObserver
オブザーバーは、Query
とコンポーネントとの間の接着剤です。Observer
はuseQuery
が呼ばれたときに生成され、常に 1 つのクエリだけを購読します。useQuery
にqueryKey
を渡さなければならないのは、このためです。 😉
Observer
が行うのはこれだけではありません。最適化の大部分がここで行われます。Observer
は、コンポーネントが使用しているQuery
のプロパティーがどれかを知っているので、無関係の変更を通知する必要がありません。
例として、data
フィールドだけを使っているのであれば、バックグラウンドで再度フェッチが行われてisFetching
に変更があっても、コンポーネントの再レンダリングは不要です。
さらにObserver
毎にselectorオプションを持ち、data
フィールドのどこに関心があるかを決定できます。 以前、#2: React Query Data Transformationsでこの最適化について書きました。
stealTime
の類やインターバルでのフェッチのようなタイマーのほとんどもまたオブザーバー層で発生します。
活性なクエリと非活性なクエリ
Observer
を持たないQuery
は、非活性なクエリと呼ばれます。どのコンポーネントからも使われていませんが、キャッシュにはまだあります。React Query DevTools を見るとき、グレイアウトした非活性のクエリを目にすることがあるでしょう。
左の数値はクエリをサブスクライブしているObserver
の数を示します。
全体像
まとめると、ほとんどのロジックがフレームワークに依存しない Query Core の部分にあります: QueryClient
、QueryCache
、Query
、QueryObserver
などが全てここにあります。
そのため、新しいフレームワークに対するアダプターをつくることは非常に簡単です。基本的に必要となるのは、Observer
を生成して、購読して、Obsever
から通知があればコンポーネントを再レンダリングする方法です。
reactやsolidのuseQuery
のアダプターは、いずれも 100 行程度のコードです。
コンポーネントの視点から
最後に、異なる視点から流れを見ましょう。コンポーネントから始まる流れです。
- コンポーネントがマウントされたら
useQuery
が呼ばれてObserver
が生成されます。 Observer
はQueryCache
にあるQuery
を購読します。- サブスクリプションは、(まだ無ければ)
Query
の生成のトリガーとなるか、データが古いと判断されたときにバックグラウンドで再度フェッチするトリガーとなります。 - フェッチが開始されると
Query
の状態が変わるので、Observer
はその変化を検知します。 Observer
はいくつかの最適化を行い、新しい状態をレンダリングする更新をコンポーネントに通知します。Query
の実行が完了したら、Observer
にそれを知らせます。
これは考えられるさまざまな流れの中の1 つにすぎないことに注意してください。理想的な状態は、コンポーネントがマウントされたとき、既にデータがキャッシュにあることです。 より詳細には、#17: Seeding the Query Cache を一読ください。
全ての流れに共通するのは、ロジックの大半が React(もしくは Solid や Vue など)の外で発生して、ステートマシーンからの全ての更新がObserver
にまで伝わって、Observer
がコンポーネントにまで伝えるべきかを決定します。