iT邦幫忙

2026 iThome 鐵人賽

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

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

Day 23|使用者還沒打完,你就先報錯了?Input 其實也有操作生命週期

  • 分享至 

  • xImage
  •  

做表單時,我很常看到一種讓人很煩的體驗:

剛點進 Input
↓
才打一個字
↓
紅框出現
↓
錯誤訊息:
「請輸入完整內容」

但問題是:

我根本還沒打完。

甚至有些欄位:

剛 focus
↓
還沒真的輸入
↓
錯誤就先出現

這種情況從程式角度看可能「驗證很即時」。

但從使用者角度看卻很像:

我還沒做完,你就先說我做錯了。

這篇想講的其實不只是一個事件。

而是:

Input 也有自己的操作生命週期。


onChange 只代表「值變了」

最常見的表單寫法:

<input
  value={value}
  onChange={e => {
    setValue(e.target.value)
  }}
/>

onChange 可以先理解成:

Input Value 發生改變。

例如:

原本:
""

輸入:
A

↓ onChange

"A"

再輸入:

AB

又會:

onChange

所以:

onChange

很適合拿來:

更新 State

但不一定代表:

現在就是最適合顯示錯誤的時間。


為什麼「每次 onChange 都 Validate」可能很煩?

例如密碼規則:

至少 8 個字元

使用者開始輸入:

a

如果立刻驗證:

只有 1 個字
↓
Error
↓
至少輸入 8 個字元

接著:

ab
↓
Error

abc
↓
Error

abcd
↓
Error

從程式角度:

規則判斷完全正確

但從 UX 角度:

使用者明明正在完成任務,畫面卻一路告訴他「你錯了」。

這就是「驗證正確」不代表「驗證時機正確」。


onBlur 是什麼?

onBlur 可以先理解成:

這個欄位失去焦點了。

例如:

點進 Email Input
↓
開始輸入
↓
輸入完成
↓
點到別的地方
↓
onBlur

這時候通常可以合理推測:

使用者暫時完成這個欄位的輸入了。

所以很多表單會選擇:

onChange
→ 更新值

onBlur
→ 驗證

而不是:

onChange
→ 每打一個字就噴錯

一個最簡單的例子

例如:

const [email, setEmail] = useState("")
const [error, setError] = useState("")

<input
  value={email}
  onChange={e => {
    setEmail(e.target.value)
  }}
  onBlur={() => {
    if (!email.includes("@")) {
      setError("Email 格式不正確")
    }
  }}
/>

資料流:

輸入中
↓
onChange
↓
更新 email
↓
先不要打擾

使用者離開欄位
↓
onBlur
↓
Validate
↓
真的不符合
↓
顯示錯誤

這種體驗通常會比:

打一個字
↓
立刻紅框

自然很多。


但 onBlur 也不是永遠最好

例如:

使用者輸入:
abc@

其實已經明顯不完整。

但他可能停在這裡很久。

如果一定要等:

onBlur

才驗證,

也可能太晚。

所以實際表單常常會做成:

第一次錯誤
→ onBlur 後才顯示

已經顯示錯誤之後
→ onChange 即時檢查是否修正

這種體驗會比較自然。


這裡常出現一個概念:Touched

可以把:

touched

理解成:

使用者真的操作過這個欄位。

例如:

一開始
touched = false

focus
↓
輸入
↓
blur
↓
touched = true

這樣 Error 顯示條件就可以從:

只要 invalid 就顯示

改成:

invalid
+
使用者真的操作過

例如概念上:

const showError =
  touched && !isValid

這樣就不會一進頁面所有欄位全部紅掉。


onFocus 又是什麼?

onFocus 是:

使用者進入這個欄位。

流程:

onFocus
↓
使用者開始操作

onChange
↓
內容正在改變

onBlur
↓
使用者離開欄位

所以一個 Input 可以先看成:

Focus
↓
Editing
↓
Blur

這已經是一個很小的 lifecycle。


中文輸入還會多一層 Composition

如果是英文:

focus
↓
onChange
↓
onChange
↓
blur

但中文、日文、韓文輸入可能還有:

focus
↓
compositionStart
↓
組字中
↓
多次 value change
↓
compositionEnd
↓
blur

所以「Input Value 變了」這件事,

其實不一定代表:

使用者已經輸入完成。


例如中文搜尋

假設使用者要輸入:

台北

輸入法組字期間可能經過:

ㄊ
台
台ㄅ
台北

如果每次:

onChange

都直接:

search(value)

就可能變成:

還在組字
↓
API 已經出發

所以我們會搭配:

onCompositionStart

和:

onCompositionEnd

去判斷:

現在是不是還在組字。


可以用 useRef 記住組字狀態

例如:

const isComposingRef = useRef(false)

開始組字:

onCompositionStart={() => {
  isComposingRef.current = true
}}

結束:

onCompositionEnd={e => {
  isComposingRef.current = false
  onSearch(e.target.value)
}}

一般輸入:

onChange={e => {
  const value = e.target.value

  if (isComposingRef.current) {
    return
  }

  onSearch(value)
}}

所以 Input 真正的流程可能是

Focus
↓
開始輸入
↓
onChange
↓
正在組字嗎?
├─ Yes
│  ↓
│  等 compositionEnd
│
└─ No
   ↓
   更新 / 搜尋

輸入完成
↓
Blur
↓
Validate

這樣比:

只要 value 改了
↓
全部事情一起做

更清楚。


為什麼 UX 常常討厭「太早報錯」?

因為 Error Message 本質上是在告訴使用者:

目前這個輸入不符合系統要求。

但如果使用者還在操作中,

這個資訊其實沒有幫助。

例如電話:

09

系統立刻顯示:

請輸入完整手機號碼

使用者心裡可能只會想:

我知道,我還在打。

所以更好的驗證時機通常要考慮:

使用者現在是否已經完成這一步?

而不是只考慮:

目前值是不是 valid?

驗證其實有兩個問題

以前我只會問:

規則是什麼?

例如:

必填
至少 8 碼
Email 格式

但實際做 UX 還要再問:

什麼時候檢查?

所以表單驗證其實有兩層:

Validation Rule
→ 什麼算錯?

Validation Timing
→ 什麼時候告訴使用者錯了?

這兩件事一樣重要。


常見的幾種策略

1. Submit 才驗證

輸入
↓
完全不提示
↓
Submit
↓
一次檢查

優點:

不打擾輸入

缺點:

可能到最後才發現很多錯

2. 每次 Change 都驗證

輸入
↓
onChange
↓
Validate

優點:

回饋很即時

缺點:

很容易太吵

3. Blur 後驗證

輸入
↓
Blur
↓
Validate

通常比較接近:

完成一個欄位後再提醒。


4. Blur 第一次驗證,之後 Change 即時修正

我自己很喜歡這種思路:

第一次操作
↓
Blur
↓
發現錯誤
↓
顯示 Error

使用者回來修改
↓
onChange
↓
一旦修正
↓
Error 即時消失

因為:

不會太早罵使用者,但也不會讓已經修好的錯誤一直掛著。


這其實就是 UI/UX 和 Frontend 交會的地方

以前做設計時可能會寫:

欄位錯誤時顯示紅框

但到了 Frontend 才會發現:

「錯誤時」到底是哪個時間點?

是:

一進頁面?

Focus?

第一個字?

Blur?

Submit?

這些都是實作時真的要決定的事情。


我現在遇到表單驗證,會多問三件事

不只:

這個欄位規則是什麼?

還會問:

① 什麼時候開始驗證?

② 使用者還在輸入時,要不要顯示 Error?

③ Error 出現後,什麼時候讓它消失?

這三個問題,

其實直接影響表單好不好用。


今天真正想記的是

onFocus
→ 開始操作

onChange
→ 值正在改變

compositionStart / End
→ 是否還在組字

onBlur
→ 離開欄位

Submit
→ 完成整份表單

它們不是一堆零散事件。

而是在描述:

使用者現在進行到哪一個階段。

所以不要只因為:

value 暫時不符合規則

就立刻告訴使用者:

你錯了。

有時候他不是輸錯。

只是:

還沒輸完而已。

下一篇可以正式離開 Input 細節,進入另一種工程判斷:

不要急著重做 Component,也許你只是還沒看懂它原本提供的參數。


上一篇
Day 22|下拉選單為什麼先閃 ID、再變成 Name?受控 Component 到底在控制什麼?
下一篇
Day 24|不要急著重做 Component,也許你只是還沒看懂它原本提供的參數
系列文
從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言