工程 · 2026.10.03 · 4 min · 更新於 2026.10.26
React 數字輸入框的坑:清空存成 0、type=number 按 e 變空字串
放貸設定頁的保留額清空後會靜靜存成 0,等於把資金全部放出去。整理修這張表單時遇到的數字輸入框問題:草稿存字串、上下限分開擋、type=number 的 e 鍵,以及 jsdom 測不到的逐字輸入。
閱讀設定
React 表單的數字輸入框清空後按儲存,存進去的可能是 0。我的放貸設定頁就是這樣,保留額欄位清空後會靜靜存成 0,等於把所有資金放出去。
修法是讓表單的草稿存使用者打的字,按儲存才解析。
同一張表單還有兩個坑。上限要在輸入當下擋、下限只能等儲存時檢查;另外,type="number" 打進 e 的當下,值會變成空字串。
後面這件事,兩千六百多個單元測試全綠也看不出來,要在真瀏覽器逐字打才會發現。
這幾個問題是我修另一批前端 issue 時順手讀到的,都出在使用者設定「保留多少錢不放貸」和「最長放幾天」的表單。
清空變成 0,等於全部放出去
保留額輸入框會把清空、打錯、負數都當成 0,而且不說。
假設一個人原本保留 5,000,他把欄位清空想重打,手滑先按了儲存,存進去的就是 0,意思是全部資金都可以放出去。
這一點我一開始也沒意會到有多嚴重,直到用這個具體的例子想過一遍。
對一般表單來說,空值變 0 只是小瑕疵;在放貸設定裡,0 是一個有意義、而且很激進的值。一個值危不危險要看領域,這種欄位的空值一定要擋。
根本原因是表單在打字當下就把字轉成數字,空字串一轉就變 0。空字串在這裡代表使用者還沒打完,不能是 0。
改法是草稿狀態存字串,按儲存時才解析,解析不了就擋下、不送任何請求,在欄位旁和 toast(畫面上跳出的短暫通知)寫明原因。
欄位旁的訊息等使用者按過一次儲存才出現,不然打到一半就一直被罵。
從 DB 讀回來的快照仍然是數字,負責算「要送哪些請求」的邏輯完全不用動,只有草稿這一層換成字串。

保留額也沒有上限。DB 欄位是 NUMERIC(20,8)(括號裡是總位數和小數位數),填到 1e12 以上,後端驗證會過、寫入時才 500。
我把上限訂在 10 億、小數最多 8 位,欄位裝得下,實際也用不到這麼大。後端驗證超過回 400,前端的範圍放在新手設定和放貸設定頁共用的檔案。
這個數字前後端各寫一次,改一邊就要改另一邊。
上限當下擋,下限儲存時查
天數是另一個問題。新手設定可以填 2 到 120 的任何天數,放貸設定頁卻只有六顆 chip(點選用的小按鈕)。
在新手設定填了 45 天的人,到設定頁就找不到對應的選項。所以設定頁也加了輸入欄,chip 留著當常用天數的快捷,兩邊讀寫同一個值。
第一版上 STG(上線前的測試環境)之後,我在天數欄打 999,它收了。
Bitfinex 本來就不能放貸超過 120 天,所以第二版改成超過 120 或不是整數的字直接不進欄位,欄位留著原本的內容。逐字打 999,欄位會停在 99;打 1200,停在 120,而且「120 天」那顆 chip 會亮。
我考慮過把超過的值夾成 120,但打 999 變成 120,等於把使用者打的值改成另一個數字。不收那個按鍵比較直接,使用者也看得出是哪一下被擋了。
下限 2 就不能這樣做,要留到儲存時檢查,因為要先打得出 1,才打得出 12。

type="number" 的 e 鍵
<input type="number"> 允許打 e、正負號和小數點,因為 1e5、-3、1.5 都是合法的數字寫法。
問題是打進這些字的當下,輸入框讀到的值是空字串。HTML 標準是這樣寫的:
User agents must not allow the user to set the value to a non-empty string that is not a valid floating-point number.
這段出自 HTML 標準的 Number state,意思是值只能是合法的數字或空字串,打到一半、還不是合法數字的內容,讀到的就是空字串。
照收的話,原本的天數就會被清成「預設」。天數只收整數,所以這幾個鍵直接在 keydown 擋掉。
這個坑是在真瀏覽器逐字輸入時才發現的。
每一項我都先寫會失敗的測試再改,單元測試全綠。但 jsdom 的 fireEvent.change 是一次把整個值塞進去,不會經過逐字輸入時的中間狀態,測試寫得再多也碰不到那一刻。
jsdom 是在 Node 裡模擬瀏覽器 DOM 的函式庫,fireEvent 的行為見 Testing Library 的文件。

所以我另外在本機用真瀏覽器配真後端,把邊界一個一個打過。保留額 1e12、10 億零 1、9 位小數、負數直接打後端,都回 400;清空保留額按儲存會被擋下;打 45 天儲存,送出去的確實是 45。
正式站上線後再實際打一次,999 只留下 99,e、小數點、正負號都打不進去。
上下箭頭用一條 CSS 關掉
數字輸入框旁邊的上下箭頭(spin button),散在全站 7 個檔案。
我沒有逐個改,用一條全域 CSS 處理,之後新加的數字欄也不會有箭頭。
直接改成 type="text" 也能拿掉箭頭,但手機鍵盤和無障礙角色都會跟著變。只是要關箭頭的話,CSS 就夠了。

工程
覆蓋率很高還是漏測?用 CRAP 指標找出最該補測試的函式
程式多半是 agent 寫的,行覆蓋率很高,卻看不出沒測到的地方是不是落在最複雜的函式。導入 CRAP 指標、門檻定在 8 之後,前後端清掉兩百多個超標函式,也修了 72 個讀程式時發現的問題。
留言
載入留言中…