前一篇講到:
Array
↓
URL
↓
String
↓
再還原回 Array
今天來看另一種很常讓人以為是「畫面自己亂跳」的問題:
下拉選單明明最後顯示是對的,為什麼中間會先閃一下 ID,再變成 Name?
這種 Bug 第一眼看起來很像:
API 太慢?
Component 有問題?
資料還沒回來?
但真正原因常常是:
同一個 Input,同時受到不同資料來源控制。
假設有一個 AutoComplete。
使用者選到某筆資料:
{
id: 123,
name: "ABC Company"
}
我們真正希望 Input 顯示:
ABC Company
但實際畫面卻會:
123
↓
閃一下
↓
ABC Company
為什麼?
因為 Component 在某一瞬間,可能先拿到了:
123
當成目前 value。
等到另一段邏輯執行後,
才把畫面改成:
"ABC Company"
所以不是畫面「自己亂閃」。
而是:
State 在短時間內被塞進了兩個不同意義的值。
一個下拉搜尋欄位,常常同時會有:
真正要提交的值
和:
使用者眼睛看到的文字
例如:
提交給 API:
123
畫面顯示:
ABC Company
這兩個其實不是同一件事。
如果把它們全部塞在同一個:
value
裡面,
就很容易出現混亂。
可以先想成:
selectedValue
→ 真正選到的是誰
inputValue
→ Input 現在顯示什麼
例如:
selectedValue = 123;
而:
inputValue = "ABC Company";
兩個都正確。
只是用途不同。
React 裡如果 Input 是這樣:
<input
value={inputValue}
onChange={e =>
setInputValue(e.target.value)
}
/>
這就是很典型的受控元件。
意思是:
Input 要顯示什麼,不是它自己決定。
而是:
State
↓
value
↓
Input
只要:
inputValue
改變,
畫面就跟著改變。
使用者輸入
↓
onChange
↓
setInputValue(...)
↓
State 更新
↓
重新 Render
↓
value={inputValue}
↓
Input 顯示新文字
所以:
畫面不是直接保存輸入值。
真正保存資料的是:
React State
Input 只是把那個 State 顯示出來。
假設選擇一筆資料時,某段邏輯先做:
setInputValue(selected.id);
也就是:
setInputValue(123);
React Render:
Input 顯示 123
接著另一段程式又:
setInputValue(selected.name);
也就是:
setInputValue("ABC Company");
下一次 Render:
Input 顯示 ABC Company
所以人眼看到:
123
↓
ABC Company
問題不在 Render。
React 只是很乖地照我們給它的 State 顯示。
我現在會先問:
這個 Input 的 value 到底是哪個 State?
例如找到:
<AutoComplete
value={inputValue}
/>
那就知道:
真正控制畫面的
=
inputValue
接著就去找:
setInputValue(...)
到底在哪些地方被呼叫。
例如:
onSelect={() => {
setInputValue(id);
}}
另外:
useEffect(() => {
setInputValue(name);
}, [name]);
這時流程可能變成:
選擇資料
↓
onSelect
↓
setInputValue(id)
↓
Render
↓
顯示 ID
接著 effect 執行
↓
setInputValue(name)
↓
再 Render
↓
顯示 Name
這就很容易造成:
閃一下
假設看到:
const [inputValue, setInputValue] =
useState("");
我現在不會只看它在哪裡宣告。
而會搜尋:
setInputValue(
看看:
誰會修改它?
什麼事件下修改?
傳進去的是 ID 還是 Name?
這招其實很實用。
因為很多「畫面突然變掉」的問題,
都不是:
誰 Render 錯了
而是:
有兩個地方都在改同一份 State
因為它同時可能有:
使用者打字
下拉選項
選中某筆資料
遠端搜尋
顯示文字
真正提交值
例如:
使用者輸入 "abc"
↓
onSearch("abc")
↓
打 API
↓
options 更新
↓
使用者選 ABC Company
↓
onSelect(...)
↓
保存 id
↓
Input 顯示 name
所以一個 AutoComplete 裡面,
其實可能同時存在:
Search Text
Selected ID
Selected Record
Display Text
Options
如果這些全部混進一個 State,
就很容易出現閃爍或不同步。
例如:
const [inputValue, setInputValue] =
useState("");
const [selectedId, setSelectedId] =
useState(null);
使用者打字:
setInputValue(text);
選中資料:
setSelectedId(record.id);
setInputValue(record.name);
這樣:
selectedId
→ 真正資料
inputValue
→ 顯示文字
角色比較清楚。
如果:
inputValue
有時候存:
123
有時候又存:
"ABC Company"
那這個 State 的語意就開始不穩定。
因為它一下代表:
ID
一下又代表:
顯示文字
這會讓後面的人很難判斷:
inputValue 到底是什麼?
所以比起一直加判斷,
更重要的是:
先把資料角色分清楚。
onChange 和 onSelect 也不是同一件事AutoComplete 常看到:
onChange
和:
onSelect
可以先簡單理解:
onChange
→ Input 文字發生改變
onSelect
→ 使用者真的選了一個 option
例如使用者只是輸入:
ABC
不代表他真的選中了:
ABC Company
所以:
輸入中的文字
和:
真正選中的資料
也應該分開思考。
如果 AutoComplete options 是 API 回來的:
Input
↓
onSearch(keyword)
↓
API
↓
Response
↓
options
↓
Dropdown
這條資料流跟:
選中資料
↓
selectedId
↓
提交表單
也是兩條不同的線。
所以完整可能是:
┌→ API → options → Dropdown
Input → keyword ─┤
│
└→ inputValue → 畫面文字
選中 option
↓
selectedId
↓
Form / Request
當這幾條線混在一起,
就會開始出現:
顯示值跳動
搜尋條件錯亂
選中的值被覆蓋
而是:
顯示內容由外部資料控制。
例如:
value={inputValue}
代表:
State 是 Source of Truth
所以 Debug 時,
不要只盯 Input 本身。
真正應該追的是:
inputValue 從哪來?
誰會修改它?
修改順序是什麼?
這是我覺得這類問題裡最重要的一點。
例如:
畫面:
ABC Company
API:
customerId = 123
那就不要假設:
一個 value 可以同時完美代表兩者
比較容易理解的模型是:
Display
→ ABC Company
Identity
→ 123
一個負責:
人看
一個負責:
程式辨認
key / identity 很像之前我們講列表:
畫面顯示 name
React 用 id 辨認資料
今天也是:
Input 顯示 name
Request 用 id 表示選中的資料
所以又回到:
顯示內容和資料 identity,不一定是同一個值。
第一步:
找到 Component 的 value
例如:
value={inputValue}
第二步:
搜尋所有 setInputValue
看:
誰塞 ID?
誰塞 Name?
第三步:
把執行順序畫出來:
選 option
↓
onSelect
↓
setInputValue(id)
↓
Render
↓
顯示 ID
另一段邏輯
↓
setInputValue(name)
↓
Render
↓
顯示 Name
只要畫完,
閃爍就不再神祕了。
這種問題如果用:
setTimeout
延遲顯示
loading 遮掉
通常只是把現象藏起來。
真正應該處理的是:
哪份 State 應該控制 Display?
哪份 State 應該保存 ID?
讓資料責任變清楚。
遇到:
畫面閃一下
值突然跳掉
Input 顯示不是預期
不要只問:
Component 為什麼 Render 兩次?
而是先問:
① 這個 Input 是誰控制的?
② value 綁哪個 State?
③ 哪些地方會修改這個 State?
④ 這個 State 是在存 ID,
還是在存顯示文字?
⑤ 這兩種資料是不是應該拆開?
因為:
React 通常不是自己亂顯示,它只是照目前的 State Render。
所以當畫面跳動,
真正值得追的通常不是:
畫面
而是:
State 是怎麼一路變成現在這個值的?
這也是我現在越來越習慣的 Debug 方法:
UI 現象
↓
找到控制 UI 的 State
↓
找到所有 State 寫入點
↓
畫出執行順序
↓
找出是哪一筆資料在不該出現的時間寫進去
下一篇可以接這個 AutoComplete 的下一個坑:
為什麼中文還沒打完,搜尋 API 就先出發了?IME、compositionStart、compositionEnd 到底在做什麼?