【React】フォームの作り方:制御コンポーネントの基本

JavaScript

こんにちは、かつコーチです。

前回はリスト表示と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;

ポイントはvalueonChangeの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;

valueonChangeは必ずセットで書く、と体に覚えさせておくと、この種の事故は防げます。

つまずき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で一元管理する書き方
  • valueonChangeは必ずセットで書く(片方だけだと入力が固まる)
  • 複数の入力欄はオブジェクト型のstateと共通ハンドラでまとめて管理できる
  • type="number"などの入力値は文字列で返ってくるため、型変換を忘れない
  • リアルタイムバリデーションのような「入力のたびに反応する」処理は制御コンポーネントの得意分野

次に読むべき記事

次回は、親コンポーネントと子コンポーネントの間でデータをやり取りする方法を解説します。

フォームの値を親コンポーネントに渡す場面などで必ず必要になる知識なので、続けて読んでみてください。

→ 次の記事:親子コンポーネント間でのデータの受け渡し方

タイトルとURLをコピーしました