前端執行時期除錯:在重新讀取原始碼之前,先檢查即時狀態
資料在網路上是正確的,但 UI 錯了。這是一個執行時期/時序錯誤,而會浪費你時間的直覺是重新讀取原始碼檔案。請抵抗這個直覺。重新調整你的反應順序。
使用時機
在除錯過程中、症狀出現時才調用——而不是在任務開始時。當下列情況全部成立時使用:
- React 元件渲染空白、過時或空的內容。
- 所需的資料在來源端是已知正確的(API/後端回傳了正確的回應)。
- 差距存在於「網路上的正確回應」與「螢幕上的正確渲染」之間。
典型描述:「API 回傳了正確資料,但元件是空白的」、「react-query 卡住了」、「請求是 200,但 UI 從未更新」、「重新整理後可以運作,但首次載入不行」。
這不適用於
- 建置/編譯錯誤 — 那是工具鏈的錯誤,不是執行時期的錯誤。
- 純樣式/版面問題 — 資料有渲染,只是看起來不對。
- API 確實回傳錯誤或遺失資料 — 那是普通的後端/合約錯誤。請先確認網路回應是否正確;如果回應確實錯誤或為空,請停下來修正後端/合約。此技能僅適用於網路回應正確但 UI 不正確的情況。
重新排序(最重要的一課)
-
在重新讀取原始碼之前,先檢查即時執行時期狀態。 直覺反應是重新讀取元件、schema、佈局和 provider 的接線,以尋找錯誤。當資料在網路上正確但 UI 錯誤時,這個直覺反應是個陷阱:錯誤通常不在單一原始碼檔案中,而是在多個各自正確的片段之間的執行時期/時序互動中。原始碼審查在結構上無法看到這類問題。請先查看即時狀態。
-
網路面板中的 200 可能具有誤導性。 顯示 200 的請求表示回應已送達。這並不表示資料已到達元件或已提交至快取。回應可能送達後被丟棄、移除或從未提交。不要從 200 就斷定「擷取正常運作」——請確認資料確實已到達消費元件的狀態。
-
對於 react-query,請檢查受影響元件的即時查詢觀察者 (query observer),而不僅僅是網路。 讀取觀察者的
status、fetchStatus、dataUpdateCount和error。- 查詢卡在
status: "pending"/fetchStatus: "fetching"且dataUpdateCount: 0表示 queryFn promise 的結果從未提交給觀察者。這指向查詢在中途被移除或無效化——並非擷取失敗。 - 透過強制觀察者重新擷取 (refetch) 來確認 queryFn 本身是否健康。如果手動重新擷取能立即且正確地解決問題,那麼擷取/客戶端/schema 都沒有問題,真正的錯誤是初始查詢被棄置了(因快取清除、effect 無效化或重新掛載而移除)。這個單一觀察就能區分「資料路徑中斷」與「資料路徑正常但查詢被終止」——這是完全不同的錯誤。
- 查詢卡在
-
一旦定位到「查詢在中途被棄置」,請向上游查看生命週期/快取管理。 原因是在查詢啟動的同一時刻執行的某些動作:
removeQueries/invalidateQueries/ 快取重置,或掛載/卸載/重新掛載的 race condition。請查看驗證/ session 轉換的 effect、主體/租戶切換的清除動作,以及 provider 的重新掛載——不要查看擷取程式碼。
決策路徑
UI 錯誤,但資料來源端正確?
→ 確認網路回應確實正確(如果不是,這不是本技能的職責)
→ 讀取受影響元件的即時查詢觀察者
狀態為 pending + fetchStatus 為 fetching + dataUpdateCount 為 0?
→ 對該觀察者強制重新擷取
重新擷取能立即且正確地解決?
→ queryFn 沒問題。錯誤 = 初始查詢被棄置。
向上游查找:removeQueries/invalidateQueries/reset、
重新掛載/卸載 race condition、驗證/session/租戶清除 effect。
重新擷取也失敗?
→ 資料路徑確實中斷。檢查 queryFn/客戶端/schema。
有資料但 UI 仍然空白?
→ 元件中的渲染/選擇器/條件錯誤,現在可以重新讀取原始碼。
即時檢查工具(指引,非穩定 API)
以下程式碼片段可從執行中的頁面,透過遍歷 React Fiber 樹從渲染的 DOM 元素開始,讀取元件的 react-query 觀察者,取得其當前的查詢狀態,並強制重新擷取以證明 queryFn 正常運作。
這些片段對版本敏感。 它們是根據 @tanstack/react-query v5.90 和 React 18 的 fiber 內部實作推導出來的,依賴於未公開的內部 API,並且會隨著版本偏移而失效。請將它們視為需要調整的起點——在使用前,請根據專案安裝的版本,重新驗證欄位名稱和 fiber 的結構。它們是指引,而非保證穩定的 API。
從 DOM 節點遍歷 Fiber 樹以尋找 hooks/觀察者:
// 選取受影響元件渲染的一個 DOM 元素(例如,透過開發者工具中的 $0)。
const el = $0; // 或 document.querySelector('[data-testid="..."]')
const key = Object.keys(el).find(k => k.startsWith('__reactFiber$'));
let fiber = el[key];
// 向上/向下遍歷 fiber 樹;memoizedState 是一個 linked list 的 hooks。
// react-query 將其觀察者儲存在某個 hook 的 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);
}
}
// 掃描 hook 狀態,尋找看起來像 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 ==> 查詢在中途被棄置,而非擷取失敗。
強制重新擷取以證明 queryFn 健康:
obs.refetch().then(r => console.log('refetch:', r.status, r.data)).catch(e => console.log('refetch failed:', e));
// 立即且正確地解決 ==> queryFn/客戶端/schema 沒問題;
// 原始查詢是在上游被終止的。
如果專案暴露了 QueryClient(例如,為了除錯而放在 window 上,或可從 provider 的 fiber 取得),則優先使用 queryClient.getQueryCache().findAll() 並直接檢查每個查詢的 state 和 observers——這比遍歷 fiber 更穩健。
完成
當你定位到錯誤時:
- 說明它落在決策分支的哪一側:「資料路徑中斷」還是「查詢被棄置」。
- 如果是被棄置,指出是哪個上游生命週期/快取動作導致。
- 只有到這時,才重新讀取負責的特定原始碼檔案——現在你知道是哪一個了。