iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
佛心分享-IT 人職涯歷練

從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始系列 第 22 篇

Day 22|下拉選單為什麼先閃 ID、再變成 Name?受控 Component 到底在控制什麼?

  • 分享至 

  • xImage
  •  

前一篇講到:

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

裡面,

就很容易出現混亂。


ID 跟顯示文字,本來就可能是兩份資料

可以先想成:

selectedValue
→ 真正選到的是誰

inputValue
→ Input 現在顯示什麼

例如:

selectedValue = 123;

而:

inputValue = "ABC Company";

兩個都正確。

只是用途不同。


什麼是 Controlled Component?

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 顯示出來。


為什麼會先閃 ID?

假設選擇一筆資料時,某段邏輯先做:

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 顯示。


Debug 時不要先問「為什麼 Input 閃?」

我現在會先問:

這個 Input 的 value 到底是哪個 State?

例如找到:

<AutoComplete
  value={inputValue}
/>

那就知道:

真正控制畫面的
=
inputValue

接著就去找:

setInputValue(...)

到底在哪些地方被呼叫。


一個 State 被多個地方修改,就要特別注意

例如:

onSelect={() => {
  setInputValue(id);
}}

另外:

useEffect(() => {
  setInputValue(name);
}, [name]);

這時流程可能變成:

選擇資料
↓
onSelect
↓
setInputValue(id)
↓
Render
↓
顯示 ID

接著 effect 執行
↓
setInputValue(name)
↓
再 Render
↓
顯示 Name

這就很容易造成:

閃一下

所以第一個 Debug 方法是:找所有寫入點

假設看到:

const [inputValue, setInputValue] =
  useState("");

我現在不會只看它在哪裡宣告。

而會搜尋:

setInputValue(

看看:

誰會修改它?
什麼事件下修改?
傳進去的是 ID 還是 Name?

這招其實很實用。

因為很多「畫面突然變掉」的問題,

都不是:

誰 Render 錯了

而是:

有兩個地方都在改同一份 State

AutoComplete 又比一般 Input 更複雜一點

因為它同時可能有:

使用者打字
下拉選項
選中某筆資料
遠端搜尋
顯示文字
真正提交值

例如:

使用者輸入 "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
→ 顯示文字

角色比較清楚。


這也是「一個 State 應該代表一個意思」

如果:

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

當這幾條線混在一起,

就會開始出現:

顯示值跳動
搜尋條件錯亂
選中的值被覆蓋

受控元件的重點不是「一定要有 useState」

而是:

顯示內容由外部資料控制。

例如:

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,不一定是同一個值。


我現在會怎麼查「先 ID 再 Name」?

第一步:

找到 Component 的 value

例如:

value={inputValue}

第二步:

搜尋所有 setInputValue

看:

誰塞 ID?
誰塞 Name?

第三步:

把執行順序畫出來:

選 option
↓
onSelect
↓
setInputValue(id)
↓
Render
↓
顯示 ID

另一段邏輯
↓
setInputValue(name)
↓
Render
↓
顯示 Name

只要畫完,

閃爍就不再神祕了。


修正重點也不是「讓 Render 慢一點」

這種問題如果用:

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 到底在做什麼?


上一篇
Day 21|多選明明是 Array,為什麼放進 URL 再回來後,竟然變成 String?
下一篇
Day 23|使用者還沒打完,你就先報錯了?Input 其實也有操作生命週期
系列文
從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言