TT Lab
はじめる
学ぶ 学習パス コース

状態管理 — ライブラリを自分で作ってみる

サーバー状態は状態ではなくキャッシュだ

TT Labで続きを見る

一言でいうと

サーバーが真実を握っている値は、状態ではなくキャッシュです。この2つを同じ入れ物に入れた途端、 「いつ古くなったのか」という問いを、コードのどこにも書く場所がなくなります。

なぜ必要なのか

ストアに、次のような形を入れたことがあるでしょう。

{ user: null, orders: [], loading: false, error: null }

ここには、性質の異なる2種類が混ざっています。userとordersはサーバーが持ち主の 値です。自分が触らなくても、ほかの人が変更します。一方、モーダルが開いているか、どのタブを 見ているかは、このブラウザだけが知っている値です。誰も裏で変更しません。

2つを同じ入れ物に入れると、サーバー側の値にだけ必要な問い、つまりいつ再取得すべきか、 今見ているものがどれくらい古いか、2つの画面が同じものを見ているか、を書く場所が ありません。そのため、その問いが画面のコードに散らばります。useEffectの中のfetch、 「更新」ボタン、ルーター遷移のときの手動リセットです。どれも同じルールに従っていないので、 1つの画面だけが古い値を表示する、ということが起きます。

React公式ドキュメントも、同じことを別の角度から述べています。レンダリング結果から計算できる 値は状態に置かないこと (You Might Not Need an Effect)、 そして状態は重複なく構造化することです (状態の構造化)。 サーバーのレスポンスをそのままコピーして持つことは、その重複のいちばん大きな形です。

どう動くのか

分ける基準は「誰が変更するのか」

問いは1つで足ります。この値を、自分以外の誰かが変更できるか。

質問 答えが「はい」 答えが「いいえ」
名前 サーバー状態 クライアント状態
例 注文一覧、残高、他人のコメント モーダルが開いているか、選択したタブ、入力中の文字
必要なもの キー、古さの判定、無効化 ただの値
保存場所 クエリキャッシュ ストアやローカル状態

サーバー状態には、値のほかにキー(key)と受信時刻が一緒に必要です。この 2つがないと、「再取得すべきか」を問うことができません。

古さ(stale)と破棄(gc)は別の物差しです

多くの人がこの2つを1つのものだと考えています。実際には、軸が2つあります。

TanStack Queryのデフォルトが、この区別をよく表しています。取得したデータは、デフォルトでは すぐに古いものとして扱います(staleTimeは0)。そのため、画面が再マウントされたり、ウィンドウに フォーカスが戻ったりすると、バックグラウンドで再取得します。一方、誰も見ていないクエリの結果は5分後に 片付けます(gcTimeのデフォルトは1000 * 60 * 5) (Important Defaults)。

このデフォルトがなぜそう決められたのかが大切です。「古い」をデフォルトにすれば、最悪の 場合でも不要なリクエストが1回増えるだけですが、「新しい」をデフォルトにすると、最悪の場合は 間違った値を表示し続けることになります。どちらの失敗が安いかは、 悩むまでもありません。

無効化は「消せ」ではなく「もう一度聞け」

注文を1件キャンセルしました。一覧のキャッシュを消すと、画面が空白で点滅してから、再び 埋まります。代わりに古いと印を付けるだけで値を残しておけば、見ていたものをそのまま表示しながら、 裏で新しい値を取得して静かに差し替えます。同じドキュメントが書いているとおり、結果は構造的に 共有され(structural sharing)、実際に変わったものがなければ参照もそのまま保たれます。st-coreで作った 参照比較とメモ化が、ここでそのまま価値を発揮します。

現場での姿

あるチームは、ログインユーザーの情報をReduxに入れ、アプリの起動時に1回だけ取得していました。 ユーザーが別のタブで名前を変えても、このタブは何時間も古い名前を表示し続け、 「一度ログアウトしてから入り直してください」が公式の案内になりました。サーバー状態を クライアント状態のように扱った代償です。

反対方向の事故もよくあります。入力中の文字をクエリキャッシュに入れておいたところ、バックグラウンドの 再取得が走って、ユーザーが打っていた文字を上書きしました。誰も裏で変更しない 値をサーバー状態として扱うと、こうなります。

分ける線がぼやけているときに使える、実戦的な基準が1つあります。タブを2つ開いておき、片方で 変更したときにもう片方が追従すべきなら、サーバー状態です。追従する必要がなければ、クライアント 状態です。この1文で、ほとんどのものは分かれます。

境界にかかる値もあります。買い物かごは、ログイン前はクライアント状態でしたが、ログイン 後はサーバー状態になります。こうしたときの答えは「両方」ではなく、どちらか一方を持ち主と決めて、 残りをそちらへ移すことです。2つの写しを同時に真実として持つと、どちらが勝つのか 誰も説明できないマージのコードが生まれ、そのコードは必ず項目を失います。

次のクイズで確認すること

どの値がサーバー状態かを分ける基準、古さと破棄がなぜ別の物差しなのか、無効化を 「消す」で組んだときに画面に何が見えるのかを、6問で確認します。