フロントエンドランタイムデバッグ:ソースコードを再読する前にライブステートを検査する
データは正しくワイヤー上を流れているのに、UI が間違っている。これはランタイム/タイミングのバグであり、時間を無駄にする本能はソースファイルを再読することです。それに抵抗し、反射神経を再順序付けしましょう。
使用するタイミング
デバッグ中、症状が現れた時点で呼び出します——タスクの開始時ではありません。以下のすべてが当てはまる場合に使用します:
- React コンポーネントが何も表示しない、古いコンテンツ、または空のコンテンツをレンダリングする。
- 必要なデータはソース(API/バックエンド)で正しいことがわかっている。
- ギャップは「ワイヤー上の正しいレスポンス」と「画面上の正しいレンダリング」の間のどこかにある。
典型的な表現:「API は正しいデータを返すがコンポーネントが空白」「react-query がスタックしている」「リクエストは 200 だが UI が更新されない」「リフレッシュ後は動くが初回ロードでは動かない」。
これが対象外のもの
- ビルド/コンパイルエラー — それはツールチェーンのバグであり、ランタイムのバグではない。
- スタイリング/レイアウトのみの問題 — データはレンダリングされているが、見た目が間違っているだけ。
- API が実際に間違ったデータまたはデータなしを返す — それは通常のバックエンド/契約バグです。最初にワイヤーレスポンスを確認してください。レスポンスが実際に間違っているか空の場合は、バックエンド/契約を修正してください。このスキルは、ワイヤーが正しいのに UI が正しくない場合のみを対象としています。
再順序付け(核心となる教訓)
-
ソースコードを再読する前に、ライブランタイムステートを検査する。 反射的にコンポーネント、スキーマ、レイアウト、プロバイダーの配線を再読してバグを探そうとします。ワイヤー上のデータは正しいのに UI が間違っている場合、その反射は罠です。バグは多くの場合、単一のソースファイルにあるのではなく、それぞれ個別には正しいピース間のランタイム/タイミングの相互作用にあります。ソースレビューはそのクラスに対して構造的に盲目です。最初にライブステートを見てください。
-
ネットワークパネルの 200 は積極的に誤解を招く可能性がある。 200 を示すリクエストは、レスポンスが 到着した ことを意味します。それはデータがコンポーネントに到達したり、キャッシュにコミットされたりしたことを意味するわけではありません。レスポンスが到着しても、破棄されたり、削除されたり、コミットされなかったりする可能性があります。200 から「fetch が機能している」と結論付けないでください——データが実際に消費コンポーネントの状態に到達したことを確認してください。
-
react-query の場合、影響を受けるコンポーネントのライブクエリオブザーバーを検査します。ネットワークだけではありません。 オブザーバーの
status、fetchStatus、dataUpdateCount、errorを読み取ります。status: "pending"/fetchStatus: "fetching"/dataUpdateCount: 0でスタックしているクエリは、queryFn のプロミスの結果がオブザーバーにコミットされなかったことを意味します。これは、クエリが途中で削除または無効化されたことを示しています——fetch の失敗ではありません。- 強制的にオブザーバーをリフェッチして queryFn 自体が健全であることを確認します。手動リフェッチが即座に正しく解決される場合、fetch/クライアント/スキーマはすべて正常であり、実際のバグは 初期 クエリが宙に浮いた(キャッシュパージによって削除された、エフェクトによって無効化された、または再マウントによって破棄された)ことです。この 1 つの観察で、「データパスが壊れている」と「データパスは正常だがクエリが強制終了された」を区別できます——まったく異なるバグです。
-
「クエリが途中で宙に浮いた」と特定したら、上流のライフサイクル/キャッシュ管理を調べます。 原因はクエリが開始したのと同時に実行されたものです:
removeQueries/invalidateQueries/ キャッシュリセット、またはマウント/アンマウント/再マウントの競合。認証/セッション遷移エフェクト、プリンシパル/テナントスイッチのパージ、プロバイダーの再マウントを調べてください——fetch コードではありません。
決定パス
UI が間違っている、データはソースで正しい?
→ ワイヤーレスポンスが実際に正しいことを確認する(正しくない場合、このスキルの仕事ではない)
→ 影響を受けるコンポーネントのライブクエリオブザーバーを読み取る
status pending + fetchStatus fetching + dataUpdateCount 0?
→ そのオブザーバーを強制リフェッチする
リフェッチが即座に正しく解決される?
→ queryFn は正常。バグ = 初期クエリが宙に浮いた。
上流を調査:removeQueries/invalidateQueries/reset、
再マウント/アンマウントの競合、認証/セッション/テナントパージエフェクト。
リフェッチも失敗する?
→ データパスが本当に壊れている。queryFn/クライアント/スキーマを調査。
データがあるのに UI がまだ空白?
→ コンポーネントのレンダー/セレクター/条件付きバグ。ここでソースを再読する。
ライブ検査ツール(リードであって、安定した API ではない)
これらは、実行中のページからコンポーネントの react-query オブザーバーに到達するために、レンダリングされた DOM 要素から React ファイバーツリーをウォークし、オブザーバーの現在のクエリ状態を読み取り、強制リフェッチを行って queryFn が機能することを証明します。
これらのスニペットはバージョンに敏感です。 これらは @tanstack/react-query v5.90 と React 18 のファイバー内部から導出されており、文書化されていない内部に依存しており、ずれるでしょう。それらを適応するための出発点として扱ってください——結果を信頼する前に、フィールド名とファイバーの形状をプロジェクトのインストール済みバージョンに対して再検証してください。これらはリードであり、安定した API が保証されているわけではありません。
ファイバーツリーを DOM ノードからウォークしてフック/オブザーバーを見つけます:
// 影響を受けるコンポーネントによってレンダリングされた DOM 要素を選択します(例:DevTools の $0 を使用)。
const el = $0; // または document.querySelector('[data-testid="..."]')
const key = Object.keys(el).find(k => k.startsWith('__reactFiber$'));
let fiber = el[key];
// ファイバーツリーを上/下にウォークします。memoizedState はフックのリンクリストです。
// react-query はそのオブザーバーをフックの memoizedState に保存します。
function* fibers(root) {
const stack = [root];
while (stack.length) {
const f = stack.pop();
if (!f) continue;
yield f;
if (f.child) stack.push(f.child);
if (f.sibling) stack.push(f.sibling);
}
}
// フック状態をスキャンして、QueryObserver のように見えるものを探します。
function findObservers(fiber) {
const found = [];
for (const f of fibers(fiber)) {
let hook = f.memoizedState;
while (hook) {
const s = hook.memoizedState;
if (s && typeof s === 'object') {
// QueryObserver インスタンスは getCurrentResult()/getCurrentQuery() を公開します。
if (typeof s.getCurrentResult === 'function') found.push(s);
// 一部のバージョンではネストされています。一般的なラッパーもチェックします。
if (s.observer && typeof s.observer.getCurrentResult === 'function') found.push(s.observer);
}
hook = hook.next;
}
}
return found;
}
const observers = findObservers(fiber);
オブザーバーのライブクエリ状態を読み取ります:
const obs = observers[0];
const r = obs.getCurrentResult();
console.log({
status: r.status, // 'pending' | 'error' | 'success'
fetchStatus: r.fetchStatus, // 'fetching' | 'paused' | 'idle'
dataUpdateCount: obs.getCurrentQuery().state.dataUpdateCount,
error: r.error,
});
// pending + fetching + dataUpdateCount 0 ==> 途中で宙に浮いた、fetch の失敗ではない。
強制リフェッチを行って queryFn が健全であることを証明します:
obs.refetch().then(r => console.log('refetch:', r.status, r.data)).catch(e => console.log('refetch failed:', e));
// 即座に正しく解決される ==> queryFn/クライアント/スキーマは正常。
// 元のクエリは上流で強制終了された。
プロジェクトが QueryClient を公開している場合(例:デバッグ用に window 上、またはプロバイダーファイバーから到達可能)、可能であれば queryClient.getQueryCache().findAll() を使用し、各クエリの state と observers を直接検査することをお勧めします。これはファイバーウォークよりも堅牢です。
完了
バグを特定したら:
- それがフォークのどちら側に該当するかを述べます:「データパスが壊れている」 vs 「クエリが宙に浮いた」。
- 宙に浮いた場合、それを強制終了した上流のライフサイクル/キャッシュアクションを特定します。
- その時点でのみ、責任のある特定のソースファイルを再読します——これでどのファイルかがわかりました。