上一篇講到:
API 重複 Request,不一定是 API 本身有問題,也可能是 Component 一直被 remount。
今天來看一個剛好相反的狀況:
我明明叫它重新抓資料了,為什麼它就是不抓?
這種 Bug 其實一樣很折磨人。
因為表面上看起來:
按鈕有按
↓
reload() 有執行
↓
畫面卻沒變
第一個反應很容易是:
reload()壞掉了?
但實際上,問題可能出在:
Request 被 cache 住了
或:
Request 被某個 key 視為「同一筆」
假設:
const { data, reload } = useRequest(
() => UserService.getList()
);
然後:
<button onClick={() => reload()}>
Reload
</button>
理想流程:
Click
↓
reload()
↓
重新執行 Request
↓
拿到新資料
↓
更新畫面
這個概念沒有問題。
但真實專案裡,Request Hook 通常不只做:
打一支 API
它可能還做:
cache
dedupe
loading 管理
request key
資料共享
所以事情就開始變複雜。
例如:
第一次 Request
GET /users
↓
Response A
↓
Cache 記住
下一次如果系統認為:
這還是同一個 Request
它可能直接拿:
Response A
而不是重新打 API。
這通常是為了:
減少重複 Request
提升速度
降低 Server 負擔
所以 Cache 本身不是壞東西。
真正的問題是:
它怎麼判斷「這是同一個 Request」?
可以把 Request Key 想成:
這次 Request 的身份證。
例如:
GET /users?page=1
可能有一個 key:
users-page-1
如果下一次又看到:
users-page-1
系統就可能認為:
我已經有這份資料了。
例如:
useRequest(fetchUsers, {
requestKey: "user-list",
});
假設這個 key:
永遠都是 user-list
但實際上資料已經因為:
新增
編輯
刪除
Filter 改變
而改變了。
如果 Request 系統還是把:
user-list
當成同一份快取資料,
那:
reload()
看起來有執行,
但拿到的可能還是舊結果。
reload() 沒跑這裡我覺得最重要。
Debug 時要分兩件事:
reload() 有沒有被呼叫?
和:
reload() 之後,Request 有沒有真的重新出去?
不是同一件事。
可以畫成:
Click
↓
reload()
↓
Request Hook
↓
Cache 判斷
├─ 使用 cache
└─ 真正發 API
所以如果只看到:
console.log("reload")
有印,
只能證明:
function 被呼叫了
不能證明:
HTTP Request 真的送出了
如果懷疑:
reload 沒有效果
我現在會直接看:
DevTools
↓
Network
然後按:
Reload
看有沒有新的 HTTP Request。
有新的 Request
那:
reload()
其實有正常工作。
接下來要看:
Response 是新的嗎?
State 有更新嗎?
UI 有吃到新的 data 嗎?
完全沒有新的 Request
那才要繼續往:
cache
request key
manual
dedupe
hook 設定
查。
例如:
reload()
↓
HTTP Request 成功
↓
Response 跟上次一樣
↓
畫面看起來沒變
這種也很常見。
所以 Debug 不要直接:
畫面沒變 = API 沒打。
而是分層:
Function
↓
HTTP Request
↓
Response
↓
State
↓
Render
每一層都要分開確認。
例如看到:
reload 失效
就想:
加新的 key
改 reqKey
關 cache
強制重新抓
這些做法有時候真的可以讓畫面恢復正常。
但有一個風險:
我可能只是把症狀壓掉,卻沒有理解原本 Request 機制。
例如:
原本 cache 設計其實有用途
結果我為了某個畫面:
把 cache 整個關掉
反而可能造成:
其他地方多打 Request
效能變差
原本共用資料失效
例如看到:
requestKey
cacheKey
reqKey
不要先刪。
先問:
誰加的?
↓
它在防什麼?
↓
它影響哪些 Request?
↓
reload 和它的關係是什麼?
這個步驟很重要。
因為專案裡很多「看起來多餘的設定」,
可能都是之前為了另一個 Bug 加的。
例如:
Page 1
Filter = ACTIVE
和:
Page 1
Filter = INACTIVE
如果兩個 Request 都被當成:
user-list
就可能發生:
不同查詢條件
↓
卻共用同一份 cache
比較合理的識別概念可能是:
user-list-page1-active
和:
user-list-page1-inactive
也就是:
會影響 Response 的條件,理論上也可能需要影響 Request 的 identity。
但實際怎麼設計,還是要看使用的 Request Library。
昨天我們講:
React key
是在回答:
這是不是同一個 Component / List Item?
今天的:
Request key
概念上也有點像:
這是不是同一個 Request?
雖然兩個不是同一個 API,也不是同一個機制,
但它們都在做一件事:
Identity
也就是:
系統要怎麼判斷「這是不是同一份東西」?
假設情境:
修改資料成功
↓
呼叫 reload()
↓
列表還是舊資料
第一步:
確認修改 API 成功嗎?
看:
PATCH / POST Response
第二步:
reload() 有被呼叫嗎?
可以:
console.log()
或 breakpoint。
第三步:
Network 有沒有新的 GET?
如果沒有:
往 Request Hook / cache 查
第四步:
如果有 GET:
Response 是舊的還是新的?
可能往:
Backend
Database
Server Cache
查。
那:
Backend 大概沒問題
往:
State
dataSource
Render
memo
查。
Save
↓
PATCH Request
↓
PATCH Success
↓
reload()
↓
GET Request 有出去嗎?
├─ No
│ ↓
│ Request Hook / Cache / Key
│
└─ Yes
↓
Response 是新的嗎?
├─ No
│ ↓
│ Backend / Database / Server Cache
│
└─ Yes
↓
State 有更新嗎?
↓
UI 有吃新的 State 嗎?
這樣 Debug 就不會變成:
一直亂改 reload。
這點我覺得也很重要。
Cache 很常被誤會成:
啊又是 cache 害的。
但其實它只是:
用舊結果換取速度
真正要處理的是:
什麼時候可以用舊資料?什麼時候一定要重新抓?
這是一個資料一致性的問題。
reload() 也不是萬能刷新按鈕我以前會把:
reload()
理解成:
強制所有東西重新來一次。
但其實它通常只是:
要求某個 Request Hook 重新執行它自己的流程。
而這個流程裡還可能有:
cache
參數
key
request lifecycle
library 行為
所以看到:
reload()
也要繼續往裡面看:
它到底 reload 了什麼?
遇到:
reload() 有執行
但資料沒更新
不要直接得出:
reload 壞了。
而是依序問:
① reload 有被呼叫嗎?
② HTTP Request 真的重新送出了嗎?
③ Response 是新資料嗎?
④ State 有更新嗎?
⑤ UI 使用的是這份 State 嗎?
如果連:
Request 都沒出去
才開始查:
cache
request key
hook 設定
這樣比較不容易在錯誤的層一直修改。
Day 19:
Request 打太多
↓
不要急著擋 Request
↓
先查 Component lifecycle
Day 20:
Request 沒有重新打
↓
不要急著怪 reload
↓
先查 Request lifecycle
共同核心都是:
不要只看症狀發生在哪一層,要把觸發流程完整畫出來。
當我開始這樣 Debug 之後,
很多原本看起來很玄學的:
React 怎麼又打 API?
為什麼不 reload?
為什麼資料還是舊的?
就會慢慢變成:
到底是哪一層沒有照我以為的方式執行?
這個問題通常比:
「要加哪段 Code?」
更接近真正的 Root Cause。
下一篇可以接一個資料跨層之後非常容易變形的實戰問題:
多選明明是 Array,為什麼放進 URL 再回來後,竟然變成 String?