多くのフロントエンドコードは、非同期処理を単一の形として扱っています。Promiseを開始し、それが解決(resolve)するのを待ち、結果をローカルステートに流し込み、フレームワークに差分を適用(reconcile)させる。このパターンは、RESTコール、フォーム送信、WebSocketメッセージなど、あらゆる場面で機能するため、非常に魅力的に見えます。それらはすべて同じ useEffect やイベントハンドラーに集まり、すべてが setState を経由し、すべてが同一の非同期的な仕組み(plumbing)のように見えます。しかし、その一様性は罠です。実際のアプリケーションにおいて、すべての非同期処理が同じであるわけではありません。それらが同じであるかのように振る舞うと、UIコンポーネントは useEffect フックと祈りに頼って継ぎ接ぎされた、意図せぬ「データアーキテクト」へと成り下がってしまいます。

非同期操作は、実際には3つの異なる種類に分類されます。それぞれが時間、キャッシュ、そして所有権に対して異なる関係を持っています。これらを見分ける術を学ぶことこそが、フロントエンドを高速で、正確で、そして健全に保つ鍵となります。

クエリ:アドレスを持つ事実

クエリは単なるフェッチではありません。それは、識別可能な事実に対するリクエストです。あなたは「何らかのユーザーデータ」ではなく、/user/123 を求めているのです。この区別は重要です。なぜなら、アイデンティティ(識別性)こそがキャッシュを可能にするからです。同じ画面上の2つのコンポーネントが同じユーザーレコードを必要とする場合、それらは一つの回答を共有すべきです。各コンポーネントが useState に独自のローカルコピーを保持すると、真実が断片化してしまいます。ヘッダーのアバターとサイドバーの名前が食い違ってしまうのは、それらが異なるタイミングでフェッチされたか、あるいは一方が成功し、もう一方が失敗したためです。

クエリはアクションではなく、リソースとして考えてください。クエリにはキャッシュキー、鮮度ポリシー、そして単一のコンポーネントの寿命を超えるライフサイクルがあります。適切に構築されたクエリレイヤーは、/projects?page=2 を読み取ることと /projects?page=3 を読み取ることが異なるものであることを理解しています。各URLとパラメータのセットが「アドレス」を形成し、そのアドレスにあるデータは、古くなっている(stale)、新鮮である、あるいは欠落している可能性があります。UIはこの管理(bookkeeping)を担うべきではありません。UIはデータレイヤーに対して user:123 を要求し、スナップショットを受け取るべきです。そのスナップショットが2秒前にサーバーから来たものか、2ミリ秒前にキャッシュから来たものかは、コンポーネントが知るべきことではありません。

実践的な影響はすぐに現れます。すべての読み取りをコンポーネント内の命令的なフェッチとして扱うと、重複排除(deduplication)ができなくなります。バックグラウンド更新も失われます。バックグラウンドで検証を行いながら、キャッシュされたデータを即座に表示する能力も失われます。クエリには、UIツリーの外側に居場所が必要です。

ミューテーション:世界を変える

クエリが世界について問いかけるものであるなら、ミューテーションは世界を変えるものです。実際のネットワークリクエスト(POSTPUT、または DELETE)は、通常、簡単な部分です。難しいのは、サーバーが「OK」と言った後に起こるすべてのことです。

例えば、ユーザーが表示名を更新したとしましょう。ミューテーション自体は単一のリクエストです。しかし、その影響範囲(blast radius)はあらゆるところに及びます。プロフィールページには古い名前が残っています。ナビゲーションバーにも古い名前が表示されています。コメント履歴が古い名前に言及しているかもしれません。もしあなたのミューテーションコードが、ローカルの isLoading フラグを切り替え、一つのステートを更新するだけで終わっているなら、あなたのアプリケーションは自分自身に嘘をついていることになります。UIの一部は変更が行われたかのように振る舞い、他の部分は変更があったことすら気づいていないのです。

ミューテーションは、データグラフに対する影響を宣言しなければなりません。どのクエリが無効になったか、どのキャッシュキーを再フェッチ(refetch)する必要があるか、どの関係性が変化したかをシステムに伝える必要があります。これはクエリとは根本的に異なります。クエリは読み取り専用であり、共有可能です。ミューテーションは書き込みに特化しており、既存のキャッシュに対して破壊的です。これらを同じ抽象化の中に押し込めてしまうと、開発者はランダムなコンポーネント内で手動で refetch() を呼び出したり、さらに悪いことに、ローカルステートをサーバーと同期させるために、ツリー全体に useEffect フックを散りばめたりすることになります。

所有権モデルも異なります。クエリは通常、キャッシュによって所有されます。ミューテーションは、それをトリガーしたユーザーアクションによって所有されます。ミューテーションには、保留状態、エラー状態、そして場合によってはロールバックが必要な楽観的な値(optimistic value)が存在します。