上一篇講到:
undefined
null
""
0
看起來都像「空」,但其實意思完全不同。
而這件事到了表單送 API 時,會直接影響資料到底有沒有被更新。
我以前就很容易遇到這種狀況:
使用者把欄位清空
↓
按 Save
↓
Request 看起來有送
↓
Status 200
↓
重新整理
↓
舊資料又回來
第一個反應通常是:
「是不是 Input 沒有真的清掉?」
但問題其實可能根本不在 Input。
而是在:
Frontend 最後送給 Backend 的值到底是什麼?
假設原本資料是:
description = "Original text";
使用者把 Input 清空。
前端現在可能拿到:
description = "";
這時候如果送:
{
description: ""
}
Backend 可能會理解成:
把 description 更新成空字串。
但如果前端最後整理 Request 時,把它轉成:
{
description: undefined
}
事情就可能完全不一樣。
undefined 有可能讓欄位根本沒被送出去假設:
const payload = {
description: undefined,
name: "Example",
};
在很多 JSON Request 的情況下,序列化:
JSON.stringify(payload);
會得到:
{
"name": "Example"
}
注意:
description
整個不見了。
也就是 Backend 實際收到的可能不是:
{
"description": null
}
而是:
{
}
根本沒有 description 這個欄位。
PATCH 通常是在表達:
只修改我這次有提供的欄位。
假設資料庫原本:
description = "Original text"
Request:
{
"description": "New text"
}
Backend 可以理解:
有 description
↓
更新它
↓
"Original text" → "New text"
但是如果 Request 變成:
{}
Backend 很可能理解成:
這次沒有要求修改 description
↓
保留原值
所以重新 GET:
description = "Original text"
就又回來了。
這時候前端看起來就會像:
明明清空了,為什麼沒有存成功?
null 的意思可能完全不同如果 API 規格支援:
{
"description": null
}
Backend 可能會把它理解成:
請明確把這個欄位清空。
資料流就變成:
Database
description = "Original text"
↓ PATCH
{
"description": null
}
↓ Backend
description = null
這和:
{}
是完全不同的意思。
可以先這樣理解:
undefined / 欄位不存在
→ 我這次沒有提供它
→ 可能代表不要修改
null
→ 我明確提供這個欄位
→ 而且我要它沒有值
""
→ 我明確提供一個空字串
但真正語意還是要看 Backend API 的 contract。
這裡是我覺得最容易誤會的地方。
畫面:
Input = ""
只是代表:
Frontend Form 現在顯示空的。
接下來還要經過:
Form Value
↓
Submit Handler
↓
資料整理
↓
Request Payload
↓
Backend
↓
Database
其中任何一步把:
""
轉成:
undefined
最後 Request 就可能不再包含這個欄位。
所以:
UI 清空成功,不代表 Request 一定有送「清空」這個意思。
例如:
const payload = {
description:
values.description === ""
? undefined
: values.description,
};
如果使用者清空:
values.description = "";
最後:
description = undefined;
接著 Request 序列化後:
description 可能直接消失
Backend 就不知道:
使用者是想清空它。
它只知道:
這次沒有 description。
那邏輯可能要改成:
const payload = {
description:
values.description === ""
? null
: values.description,
};
這時:
Input 清空
↓
values.description = ""
↓
轉成 null
↓
Request
Payload:
{
"description": null
}
Backend 才有機會理解:
明確清空這個欄位。
這裡也不能直接下結論:
那以後所有空字串都轉 null 就好了。
不一定。
因為不同 API Contract 可能有不同定義。
例如:
欄位 A
null
→ 清除資料
欄位 B
""
→ 合法的空字串
欄位 C
undefined
→ 不修改
欄位 D
null
→ Backend 不接受
所以真正要先確認的是:
這支 API 對這個欄位的規格到底是什麼?
以前我會一直盯:
Input 有沒有清掉?
現在遇到這種問題,我會先開 DevTools:
Network
↓
找到 PATCH Request
↓
看 Request Payload
確認實際送的是:
{
"description": ""
}
還是:
{
"description": null
}
還是根本變成:
{}
這一步很重要。
因為它直接把問題切成兩半。
例如使用者明明要清空,但 Request:
{}
那問題大概率還在 Frontend。
可以繼續找:
Form Value
↓
onFinish / Submit
↓
資料整理 function
↓
payload
看看是哪一段把值吃掉。
假設 Request:
{
"description": null
}
完全符合 API 規格。
但重新 GET:
{
"description": "Original text"
}
那就不用一直改 Input 了。
接下來應該往 Backend 查:
Controller 有收到 null 嗎?
↓
Request DTO 接得住嗎?
↓
Service 有沒有忽略 null?
↓
Entity 有更新嗎?
↓
Repository 有 save 嗎?
↓
Database 最後是什麼?
這就接回 Day 10 的整條資料流。
例如後端邏輯如果寫成類似:
request.description?.let {
entity.description = it
}
可以先把它理解成:
description 有值
↓
才更新
description = null
↓
這段不執行
這種寫法對某些欄位很合理。
因為 Backend 可能把:
null
理解成:
不更新。
但如果需求本來就是:
null代表清空,
那這段邏輯就可能需要另一種處理方式。
所以同樣是:
null
前後端一定要對它的意思有共同約定。
以前我會把 API 想成:
Frontend 丟資料
↓
Backend 收資料
但真正合作後,會發現中間其實有一份很重要的契約:
這個欄位是必填嗎?
可以 null 嗎?
空字串代表什麼?
欄位沒送代表什麼?
PATCH 時 null 是清除還是不修改?
這些都是:
API Contract
的一部分。
所以如果 Frontend 覺得:
null = 清空
但 Backend 覺得:
null = 不修改
兩邊的程式可能都「沒有語法錯誤」。
結果功能還是錯的。
假設需求是:
清空 description 並儲存。
我會從最靠近使用者的地方開始:
① Form Value
使用者清空後
到底是:
""
null
undefined?
接著:
② Submit Handler
有沒有重新轉換資料?
再來:
③ Request Payload
DevTools Network
真正送出去的是什麼?
再往後:
④ Backend
API 規格期待什麼?
最後:
⑤ GET Response
儲存之後重新取得
實際回來的是什麼?
完整就是:
UI
↓
Form Value
↓
Submit
↓
Payload
↓
Backend
↓
Database
↓
GET Response
↓
UI
只要一層一層確認,就比一直猜:
「是不是 React 沒更新?」
有效很多。
undefined
→ 欄位可能不會出現在 JSON Payload
→ PATCH 時常可能等於「沒要求修改」
null
→ 欄位有送出去
→ 明確表示沒有值
→ 是否代表清空,要看 API Contract
""
→ 一個真正的空字串
→ Backend 是否接受,也要看 API Contract
所以:
Input 清空,只是整條資料流的第一步。
真正要知道有沒有清空成功,還得一路追到:
Request Payload
↓
Backend
↓
Database
而我現在遇到這種 Bug,也不會再只問:
「畫面有沒有清掉?」
而會問:
「清空這件事,在這支 API 裡到底用什麼值表示?」
下一篇可以繼續接另一個我以前很容易踩的坑:
||、??、三元運算子看起來都在給預設值,到底什麼時候該用哪一個?