【第21回】入力を受け取っても壊れないようにする|フォーム(嫁子が教えるTS講座)
今回のゴール
- 入力欄の値をstateで受け取れる
- 送信でページが再読み込みされるのを止められる
- 外から来る値は型では守れないという話を、実物で確認する
前回(useState / useEffect)で items を持てるようになりました。今回は、そこに入力を足します。全体の流れは講座の目次へ。
嫁子: 今日は第11回で言ったことの答え合わせでもあるの。
「型が守ってくれるのは自分たちが書いたコードの内部だけで、外から来るデータは守れない」って話ね。フォームは、まさにその外側との境界だよ。
① 入力欄を state とつなぐ
第9回では input.value を直接読んでいました。Reactでは、入力欄の値もstateで持ちます。
"use client";
import { useState } from "react";
export default function AddForm() {
const [name, setName] = useState("");
return (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
value に state を渡し、onChange で state を更新しています。入力するたびに setName が呼ばれ、その値が value として戻ってくるという往復です。
回りくどく見えますが、これで「画面に見えている値」と「プログラムが持っている値」が絶対にズレなくなります。
value だけ渡して onChange を書かないと、文字が入力できなくなります。stateが変わらないので、表示も変わらないためです。よくある詰まりどころです。
嫁子: 第9回では、必要になった瞬間に
input.valueを読みに行ってたでしょ。あれは「見に行く」方式。Reactは「常に持っている」方式なの。どっちが正か迷わなくて済むのが利点だね。第10回で言った「データが正で、画面はその写し」が、ここでも同じだよ。
② 送信を受け取る
ボタンだけでも動きますが、<form> を使うとEnterキーでも送信できるようになります。
"use client";
import { useState } from "react";
type AddFormProps = {
onAdd: (name: string) => void;
};
export function AddForm({ onAdd }: AddFormProps) {
const [name, setName] = useState("");
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const trimmed = name.trim();
if (trimmed === "") {
return;
}
onAdd(trimmed);
setName("");
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="牛乳"
/>
<button type="submit">追加</button>
</form>
);
}
3つ、押さえるところがあります。
e.preventDefault() は必須です。書かないとページが再読み込みされ、stateが全部消えます。HTMLのフォームは本来サーバーに送信するものなので、その動作を止めています。
onAdd はPropsで受け取った関数です。第19回のPropsと同じ仕組みで、値だけでなく関数も渡せます。追加処理そのものは親が持ち、この部品は「名前が決まったら知らせる」ことに徹しています。
trim() で空白だけの入力を弾いています。第10回でやったのと同じ処理です。
③ 親側で受け取る
"use client";
import { useState } from "react";
import { AddForm } from "./AddForm";
type Item = {
id: number;
name: string;
done: boolean;
};
export default function Page() {
const [items, setItems] = useState<Item[]>([]);
function handleAdd(name: string) {
setItems([...items, { id: Date.now(), name: name, done: false }]);
}
return (
<>
<AddForm onAdd={handleAdd} />
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</>
);
}
入力の管理は子、データの管理は親という分担です。子は自分の入力欄だけを見ていればよく、親は items だけを見ていればよくなります。
嫁子: この分け方、AIに指示を出すときの単位そのものになるよ。
「入力欄の挙動を直して」なら
AddForm、「追加した後の並び順を変えて」なら親。どっちの話かを言えるだけで、返ってくるものの精度が変わるからね。
④ 数値の入力は、文字列で届く
第4回で予告したことが、ここで現実になります。
const [price, setPrice] = useState("");
// price は string。type="number" にしても string で届く
<input type="number" value={price} onChange={(e) => setPrice(e.target.value)} />
type="number" と書いても、e.target.value は文字列です。そのまま計算に使うと、"1000" + 200 が "1000200" になります。
数値として扱うなら、明示的に変換します。
const num = Number(price);
if (Number.isNaN(num)) {
// 数値にならなかったときの処理
}
空文字を Number() に渡すと 0 になります。「未入力」と「0」を区別したいときは、変換の前に空かどうかを見てください。
| 入力 | Number() の結果 |
|---|---|
"1000" |
1000 |
""(空) |
0 |
"あ" |
NaN |
嫁子: ここ、TypeScriptを使っていても防げないところなの。
だって型の上では最初から
stringで、正しくstringとして扱われてるからね。間違っているのは型じゃなくて、こっちの想定なの。型は万能じゃないよ。外から入ってくるところは、自分で確かめるしかない。
⑤ AIに頼むときに書いておくこと
フォームは条件を書き落としやすい場所です。指示に入れておくと事故が減ります。
| 指定すること | 書かないとどうなるか |
|---|---|
| 空・空白だけの入力を弾く | 空の項目が追加される |
数値は Number() で変換し、NaN を確認 |
計算結果がおかしくなる |
| 送信後に入力欄を空にする | 前の値が残り続ける |
e.preventDefault() を入れる |
送信でページが再読み込みされる |
「入力してほしい値」ではなく、「入力されうる値」で考えるのがコツです。空欄や記号だけの入力、極端に長い文字列など、人は必ず想定外を入れてきます。
嫁子: 私が見てきた中で、いちばん多い抜けは空欄の扱いだと思う。
動作確認するとき、みんな真面目に「牛乳」って入れるでしょ。だから空のまま押す人がいることに気づかないの。テストは、意地悪な人になったつもりでやるといいよ。
つまずきポイント
onChange の e.target.value は常に文字列です。チェックボックスだけは e.target.checked(真偽値)を使います。
value に null や undefined を渡さないでください。入力欄が「管理されている状態」から外れて、警告が出ます。空にしたいときは空文字 "" を使います。
型が思い出せないときはAIに聞けば済みます。React.FormEvent<HTMLFormElement> のような型名を覚える必要はありません。
今回のまとめ
- 入力欄は
valueとonChangeの両方をstateにつなぐ(片方だけだと入力できない) <form>の送信ではe.preventDefault()を必ず書く(書かないとstateが消える)- Propsでは関数も渡せる。入力の管理は子、データの管理は親
type="number"でも値は文字列。Number()で変換し、空文字が0になる点に注意- 型で守れるのは内部だけ。外から来る値は自分で確かめる
次にやること
次回はTailwind CSSで見た目を整えます。第4部の最後で、ここまで素っ気なかった画面に手を入れます。
今回の宿題:上のフォームを作って、空のまま追加ボタンを押してみてください。何も起きなければ成功です。そのあと trim() の行を消して、空白だけの項目が追加されるのも見ておくと、なぜ要るのかが分かります。
嫁子: わざと壊す確認、この講座で何回か出てきたよね。
正しく動くことより、おかしな入力で壊れないことのほうが確かめにくいの。だから意識してやるしかないんだよ。