【第21回】入力を受け取っても壊れないようにする|フォーム(嫁子が教えるTS講座)

今回のゴール

前回(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> のような型名を覚える必要はありません。


今回のまとめ


次にやること

次回はTailwind CSSで見た目を整えます。第4部の最後で、ここまで素っ気なかった画面に手を入れます。

今回の宿題:上のフォームを作って、空のまま追加ボタンを押してみてください。何も起きなければ成功です。そのあと trim() の行を消して、空白だけの項目が追加されるのも見ておくと、なぜ要るのかが分かります。

嫁子: わざと壊す確認、この講座で何回か出てきたよね。

正しく動くことより、おかしな入力で壊れないことのほうが確かめにくいの。だから意識してやるしかないんだよ。