こんにちは、かつコーチです。
前回はリスト表示とkey属性の重要性について解説しました。
今回は、Reactでの開発において避けて通れない「フォーム」の作り方を扱います。
ログイン画面や検索窓、お問い合わせフォームなど、ユーザーの入力を受け取る場面は数え切れないほど登場するので、ここでしっかり基礎を固めておきましょう。
制御コンポーネントとは?
「制御された」とはどういう意味か
Reactでフォームを扱うときによく出てくるのが「制御コンポーネント(Controlled Component)」という言葉です。
制御コンポーネントとは、入力欄(<input>など)の値を、Reactのstateで一元管理する書き方のことです。
HTMLの<input>は、本来ブラウザが自分自身で入力値を保持しています。
しかしReactでは、その値をstateに持たせて「今画面に表示されている値は、常にstateの値と一致している」という状態を作ります。
これにより、入力値をいつでも読み取れますし、バリデーションや送信処理もReactの世界の中で完結させられます。
なぜstateで管理する必要があるのか
「ブラウザ任せにしておけばいいのでは?」と思うかもしれません。
ただ、それだと次のようなことがやりにくくなります。
- 入力中にリアルタイムでバリデーションしたい
- 特定の条件で入力を制限したい(半角数字のみなど)
- 入力値を別のコンポーネントと連動させたい
こうした「入力値をプログラムでコントロールしたい」場面が出てきたときに、制御コンポーネントの考え方が生きてきます。
基本の書き方
テキスト入力を制御する
まずは一番シンプルなテキスト入力の例です。
import { useState } from "react";
function NameForm() {
const [name, setName] = useState("");
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>入力中の名前: {name}</p>
</div>
);
}
export default NameForm;
ポイントはvalueとonChangeの2つです。
value={name}で「入力欄に表示する値はstateのname」と指定し、onChangeで「入力があるたびにstateを更新する」処理を書きます。
この2つがセットになって初めて、入力欄がReactの制御下に入ります。
submitイベントで送信処理をまとめる
フォーム全体を送信するときは、<form>のonSubmitを使います。
import { useState, FormEvent } from "react";
function ContactForm() {
const [email, setEmail] = useState("");
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("送信されたメールアドレス:", email);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">送信</button>
</form>
);
}
export default ContactForm;
e.preventDefault()を書き忘れると、ブラウザ標準のページ遷移が発生してしまうので注意しましょう。
これはHTMLの<form>タグが本来持っている「送信ボタンを押すとページをリロードする」という挙動を止めるためのおまじないです。
複数の入力欄をまとめて管理する
入力欄が増えてくると、1つずつstateを分けるのが面倒になってきます。
そんなときはオブジェクト型のstateにまとめて、共通のハンドラで更新するのが定番のやり方です。
import { useState, ChangeEvent } from "react";
type FormData = {
name: string;
email: string;
};
function ProfileForm() {
const [formData, setFormData] = useState<FormData>({
name: "",
email: "",
});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
return (
<form>
<input name="name" value={formData.name} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
</form>
);
}
export default ProfileForm;
<input>側のname属性とstateのキーを合わせておくことで、1つのhandleChange関数だけで複数の入力欄を処理できます。
よくあるつまずきポイント・エラー対処
つまずき1:valueだけ書いてonChangeを忘れる
私が初めてこのパターンを書いたとき、valueだけ指定してonChangeを書き忘れ、入力欄に一切文字が打てなくなったことがあります。
コンソールにも「You provided a value prop to a form field without an onChange handler」という警告が出ていたのですが、最初は何のことか理解できず、しばらく固まってしまいました。
❌ Before:onChangeがなく、入力欄が固まってしまう
import { useState } from "react";
function BrokenForm() {
const [text, setText] = useState("");
return <input type="text" value={text} />;
}
export default BrokenForm;
このコードでは、valueが常に空文字に固定されているため、どれだけキーボードで入力しても画面には反映されません。
Reactが「表示する値」を毎回stateから決め直しているのに、そのstateを更新する手段がないためです。
✅ After:onChangeでstateを更新する
import { useState } from "react";
function FixedForm() {
const [text, setText] = useState("");
return (
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
);
}
export default FixedForm;
valueとonChangeは必ずセットで書く、と体に覚えさせておくと、この種の事故は防げます。
つまずき2:数値入力でstateの型がずれる
もう一つ実務でハマったのが、type="number"の入力欄です。
e.target.valueはどんなtypeの<input>でも常に文字列で返ってくるため、数値として扱いたい場合は明示的に変換が必要です。
import { useState, ChangeEvent } from "react";
function AgeForm() {
const [age, setAge] = useState<number>(0);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setAge(Number(e.target.value));
};
return <input type="number" value={age} onChange={handleChange} />;
}
export default AgeForm;
この変換を忘れて文字列のまま計算処理に渡してしまい、"10" + "5"が"105"になってしまう、という単純だけれど気づきにくいバグを経験したことがあります。
数値を扱うフォームでは「今この値は文字列か数値か」を常に意識するようにしましょう。
応用・一歩先の使い方
非制御コンポーネントとの違い
制御コンポーネントの対になる概念として「非制御コンポーネント(Uncontrolled Component)」があります。
こちらはuseRefを使ってDOMから直接値を取得する方式で、stateによる再レンダリングが発生しない分、ファイルアップロードのような場面で扱いやすくなります。
ただし、リアルタイムバリデーションのような「入力のたびに反応したい」処理には向いていません。
基本的には制御コンポーネントを使い、DOM操作がどうしても必要な場面だけ非制御コンポーネントを検討する、という使い分けが現場ではよく見られます。
入力値のリアルタイムバリデーション
制御コンポーネントの強みは、入力のたびに任意の処理を挟めることです。
import { useState, ChangeEvent } from "react";
function PasswordForm() {
const [password, setPassword] = useState("");
const isValid = password.length >= 8;
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
};
return (
<div>
<input type="password" value={password} onChange={handleChange} />
{!isValid && password.length > 0 && (
<p style={{ color: "red" }}>8文字以上で入力してください</p>
)}
</div>
);
}
export default PasswordForm;
このように、入力値の変化に応じて即座にフィードバックを返せるのが制御コンポーネントの大きなメリットです。
まとめ
この記事のポイント
- 制御コンポーネントとは、入力欄の値をReactのstateで一元管理する書き方
valueとonChangeは必ずセットで書く(片方だけだと入力が固まる)- 複数の入力欄はオブジェクト型のstateと共通ハンドラでまとめて管理できる
type="number"などの入力値は文字列で返ってくるため、型変換を忘れない- リアルタイムバリデーションのような「入力のたびに反応する」処理は制御コンポーネントの得意分野
次に読むべき記事
次回は、親コンポーネントと子コンポーネントの間でデータをやり取りする方法を解説します。
フォームの値を親コンポーネントに渡す場面などで必ず必要になる知識なので、続けて読んでみてください。
→ 次の記事:親子コンポーネント間でのデータの受け渡し方