TL;DR
input[type=number] には空文字とかも入力できるので、string 型で状態管理をしないと意図しない動きをすることがある
ステートの型を縛るより input の value に渡るまでの実装を工夫する方が良い
---
input[type=number] のステートを number 型にするとだいたい後悔する
フォームで const [value, setValue] = useState<number>(0) を見かけたら警戒する
value を更新する際にnumber にキャストして setState をする実装はおかしな挙動を生む
文字を全部消したらなぜか勝手に 0 が入力されてた、というのはこれによって生じる
この 0 は消せないので、値を修正しようとしたら頭に変な 0 がついてきたとかが起こる
https://scrapbox.io/files/633ade0e2efe22001d60e1ea.gif
この例では Number() でキャストしている
Number('') === 0
parseInt() を使った場合は NaN になる
input type="number" の value に NaN を渡すと React でエラーが出る
これらについて「 Number() や parseInt() の挙動が変なのが悪いんじゃないか」と思う人がいるかもしれないが、それは間違っている
確かに 0 になったり NaN になったりするのは覚えにくい
が、そもそも入力欄をすべて消したときのステートに 0 や NaN を割り当てるのがおかしい
'' や null を入れたほうが良い
min={0} を設定しているようなケースは勝手に 0 が入力されても良いはず
が、この場合も number へのキャストによって IME の問題(後述)が発生しうる
全角で入力して IME で半角数字に変換するのは本来の input[type=number] では動くはずだが、動かなくなる
https://scrapbox.io/files/633ae000c6dd200023ef7608.gif
どうすれば良いのか
あんまりコレという結論が出てない
Number.isNaN(value) で分岐する
ステートは string で、value= にわたすタイミングで number にする
code:typescript
function parseValue(value: string) {
const parsed = parseInt(value, 10)
if (Number.isNaN(parsed)) {
return ''
}
return parsed
}
return (
<form>
<input
type="number"
value={parseValue(value)}
onChange={e => setValue(e.currentTarget.value)}
/>
</form>
);
TIPS: こういう時に globalThis.isNaN() を使うとダメで、Number.isNaN() を使うべき
空文字以外の理由で NaN になるケースを全部空文字とするのが妥当かは微妙?
が、上にあげた問題が起こらなくなっている
https://scrapbox.io/files/633ae154bcd8700022877801.gif
空文字を 0 ではなく null や undefined とする
別解
code:typescript
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const parsed = parseInt(e.currentTarget.value, 10);
if (Number.isNaN(parsed)) {
setValue(null);
} else {
setValue(parsed);
}
}
return (
<form action="">
<input type="number" value={value ?? ""} onChange={handleChange} />
</form>
);
?? '' を忘れると意図せず uncontrolled component になってしまう
てっきり、入力の途中で e.currentTarget.value と異なる値(ここでは null )を入れるとまずかったりしないんだろうかと思っていたがそんなことはないらしい
string で持たないと IME で入力できない問題が残ると勘違いしていた
ふつうに value を string で受け、onChange で string が入るような形で返す
ただし、入れていい値をバリデーションする( /\d+/ または '' を許すなど )
あるいは末端のコンポーネントは onChange を (value: string) => void にしない
ふつうに React.ChangeEvent<HTMLInputElement> を返す
これは親コンポーネントに問題を押し付けてるだけと言えるかも