前 18 天,我們一路從:
資料在哪裡?
↓
State / Props
↓
Component
↓
useEffect
↓
useRequest
↓
API
↓
Render
↓
key
慢慢把 React 和資料流接起來。
從今天開始,我想正式進入另一個階段:
不是再學一個新語法,而是拿這些觀念去 Debug 真實問題。
第一個案例,就是我曾經遇過的一個很典型的問題:
使用者只是展開 Filter,為什麼 API 又重新打了一次?
假設頁面上有一個搜尋區塊:
搜尋條件
↓
展開更多 Filter
↓
顯示更多欄位
需求看起來很單純。
但實際操作時卻發現:
第一次進頁面
↓
API Request
點「展開」
↓
API Request 又來一次
點「收合」
↓
又 Request
第一個反應很容易是:
是不是 API function 被多呼叫了?
於是開始找:
request()
reload()
useEffect()
useRequest()
但後來真正的問題不一定在 Request 本身。
而是在:
Filter Component 被重新建立了。
這裡很容易混。
React Component 可以:
重新 Render
但仍然是同一個 Component instance。
也可能:
原本 Component 被移除
↓
unmount
重新出現
↓
mount
這就是完全不同的事情。
可以先簡單分:
Re-render
→ 同一個 Component 再算一次畫面
Remount
→ 舊 Component 消失
→ 建立一個新的 Component
這兩件事對 API Request 的影響可能差很多。
假設某個 Filter Component 裡有:
useRequest(fetchOptions);
或某種「Component 建立時自動載入資料」的邏輯。
第一次:
Filter mount
↓
Request
↓
取得 options
如果只是正常 re-render,
Component 沒有被摧毀,
不一定會重新走一次初始化。
但如果展開 / 收合的寫法造成:
Filter unmount
↓
Filter mount
那對 React 來說:
這是一個新的 Component。
所以:
初始化
↓
Request
又會再跑一次。
例如:
{showFilter && (
<FilterPanel />
)}
當:
showFilter = true
畫面:
FilterPanel mount
當:
showFilter = false
畫面:
FilterPanel unmount
再改回:
showFilter = true
又會:
建立新的 FilterPanel
↓
mount
所以如果裡面有初始化 Request,
它就可能重新發生。
這也是我後來才比較有感的地方。
對使用者來說:
收合 Filter
看起來只是:
不要顯示。
但程式寫法可能是:
showFilter
? <FilterPanel />
: null
這代表:
不顯示
=
Component 根本不存在
所以:
展開
→ 建立
收合
→ 刪掉
再展開
→ 再建立
這就不是單純的 UI 顯示控制。
假設真正需求只是:
Filter 收起來時不要看到,但它的狀態和初始化不要消失。
那就可以考慮:
Component 保留
↓
只控制 CSS 顯示
例如概念上:
<div
style={{
display: showFilter
? "block"
: "none",
}}
>
<FilterPanel />
</div>
這時候:
FilterPanel
一直都存在。
只是:
看得到 / 看不到
不同。
資料流就變成:
第一次 mount
↓
Request 一次
之後展開 / 收合
↓
Component 還在
↓
只改 display
↓
不需要重新初始化
以前我可能會覺得:
都 React 了,怎麼還用 CSS hide?
但後來發現,真正要看的是需求。
如果需求是:
不需要保留 Component
那 unmount 很合理。
但如果需求是:
只是暫時不要顯示
↓
State 要保留
↓
不希望初始化重新發生
那保留 Component、只控制顯示,反而可能更符合需求。
所以不是:
CSS 比 React 差
而是:
你到底想「隱藏」,還是想「移除」?
遇到重複 Request 時,很自然會想:
能不能加條件?
↓
不要重複打
例如:
加 cache
加 reqKey
加判斷
改 reload
這些方法可能真的能把 Request 擋掉。
但如果 Root Cause 是:
Component 一直被 remount,
那我們其實是在 Request 層幫 UI 結構擦屁股。
這樣做可能會讓問題越來越複雜。
表面症狀:
API 重複 Request
真正原因可能是:
Filter 展開 / 收合
↓
Component unmount / mount
↓
初始化重新執行
↓
Request 重打
所以如果只盯著:
Request
很容易一直在錯的層修。
這也是我後來越來越常提醒自己的:
Bug 出現在哪裡,不代表 Root Cause 就在哪裡。
第一步:
先重現
進頁面
↓
Request 一次
點展開
↓
Request 第二次
確認真的和:
showFilter
有關。
第二步:
找展開 / 收合控制的是什麼
例如:
{showFilter && <FilterPanel />}
第三步:
問:
showFilter 改變後
FilterPanel 是:
重新 Render?
還是 unmount → mount?
第四步:
再看 FilterPanel 裡:
誰會在 mount 時 Request?
例如:
useRequest
useEffect
Component 初始化
最後才得到完整流程:
showFilter 改變
↓
FilterPanel 被移除 / 建立
↓
初始化重新執行
↓
API Request
這樣 Root Cause 才完整。
因為 Request 本身可能完全正常。
例如:
Component mount
↓
Load options
這本來就是正確行為。
真正錯的是:
Component 不應該因為單純展開 / 收合就一直重新 mount。
所以比較合理的修正可能是:
保留 Component
↓
用 display 控制顯示
而不是:
Request 多加十個條件
如果只知道:
API 重複打
很容易只查 API。
但前面我們已經學過:
Component
State
Render
useRequest
Effect
key
所以現在可以開始畫:
showFilter State
↓
Render
↓
Component 是否存在?
↓
mount / unmount
↓
Request lifecycle
這就是我覺得從「會語法」走向「會 Debug」很重要的一步。
遇到:
某個操作
↓
API 突然又 Request
不要第一個就問:
「API 怎麼不要重打?」
先問:
這個操作有沒有讓 Component 被重新建立?
State 有沒有被重置?
Effect / Request 初始化是不是又跑了一次?
也就是:
先找是誰觸發 Request,再決定該在哪一層修。
最後這個案例的重點對我來說不是:
CSS display 比 conditional rendering 好
而是:
真正的解法要對準 Root Cause。
有時候 API 重複 Request,
真正要修的並不是 API。
下一篇可以接另一個很類似的 Debug 思考:
明明有 reload(),為什麼資料就是不重新抓?Cache、Request Key 到底是在幫忙還是在搗亂?