モダンJavaScriptの基礎から始める挫折しないためのReact入門 60日コース | フォーム・コンポーネント設計 - DAY 33:複数フォーム

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 33:複数フォーム 前半 ― 「項目が増えても怖くない」フォームの基本設計
  2. 複数フォームの基本方針を決める
    1. 「項目ごとにStateを持つ」から始める
  3. 名前・メール・年齢をControlled Componentで作る
    1. 名前(text)入力欄
    2. メール(email)入力欄
    3. 年齢(number)入力欄
  4. textareaとselectをControlled Componentで扱う
    1. 自己紹介(textarea)
    2. コース選択(select)
  5. 複数フォームを1つのコンポーネントとしてまとめる
    1. フォーム全体のコード(DAY 33 前半版)
  6. DAY 33 前半のまとめ ― 「複数フォームでも型は同じ」
  7. DAY 33:複数フォーム 中盤 ― 「複数項目×Controlled Component」を設計目線で整理する
  8. 複数フォームを「1つのデータセット」として捉える
    1. 項目ごとのStateを「1枚のカード」としてイメージする
  9. 各項目とState・onChangeの対応関係をはっきりさせる
    1. 名前入力欄:text+State+onChange
    2. メール入力欄:email+State+onChange
    3. 年齢入力欄:number+State+onChange
    4. 自己紹介:textarea+State+onChange
    5. コース選択:select+State+onChange
  10. 複数フォームを「Controlled Componentの集合」として見る
    1. フォーム全体を分解してみる
  11. フォーム全体の送信処理を設計する
    1. handleSubmitで「1枚のカード」として扱う
  12. 中盤版テンプレート:複数フォーム+軽いチェック
  13. DAY 33 中盤のまとめ ― 「複数フォーム=複数のControlled Component+1枚のカード」
  14. DAY 33:複数フォーム 後半 ― 「実戦で使える複数フォーム」に仕上げる
  15. 複数フォームに「最低限のバリデーション」を組み込む
    1. まずは何をチェックするかを決める
  16. エラー状態を「派生情報」として扱う
    1. Stateそのものではなく「Stateから計算される値」としてエラーを考える
  17. エラーメッセージをフォームに自然に組み込む
    1. 項目ごとにエラーメッセージを表示する
  18. 送信処理に「バリデーションの最終チェック」を入れる
    1. handleSubmitで「最後の門番」をする
  19. DAY 33 後半版:複数フォーム+バリデーション付きテンプレート
  20. DAY 33 後半のまとめ ― 「複数フォーム+バリデーション」が実戦の基本形

DAY 33:複数フォーム 前半 ― 「項目が増えても怖くない」フォームの基本設計

DAY 33の前半では、 名前・メール・年齢・textarea・select をまとめて扱う「複数フォーム」を題材に、 項目が増えても迷わないフォーム設計の基本を身につけていきます。

ここまでで、単一の入力欄(名前入力フォーム)で

ユーザー入力 → onChange → State更新 → 画面に反映

という流れは体験済みです。 今回は、それを 複数項目にそのまま広げる ことがテーマになります。

複数フォームの基本方針を決める

「項目ごとにStateを持つ」から始める

まずは、シンプルに「項目ごとにStateを持つ」方針で考えます。

扱う項目は次の5つです。

  • 名前(text)
  • メール(email)
  • 年齢(number)
  • 自己紹介(textarea)
  • コース選択(select)

それぞれに対応するStateを用意します。

import { useState } from "react";

function MultiFormBasic() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState("");
  const [bio, setBio] = useState("");
  const [course, setCourse] = useState("react");

  return (
    <div>
      <h2>DAY 33:複数フォーム(前半)― 基本のState設計</h2>
      {/* ここにフォームを作っていきます */}
    </div>
  );
}

export default MultiFormBasic;
JSX

ここでのポイントは、

  • 名前・メール・年齢・自己紹介・コース選択が、それぞれ独立したStateを持っていること
  • course の初期値を "react" にして、selectの初期選択を決めていること

です。

この「項目ごとにStateを持つ」という方針は、 複数フォームの一番わかりやすいスタート地点になります。

名前・メール・年齢をControlled Componentで作る

名前(text)入力欄

まずは、名前入力欄を作ります。

<div>
  <label>
    名前:
    <input
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="名前を入力してください"
    />
  </label>
</div>
JSX

ここでは、

  • value={name} で入力欄の表示をStateに紐づけています。
  • onChange で入力のたびに setName を呼び、Stateを更新しています。

単一フォームで学んだ「Controlled Component」の基本形が、そのまま使えています。

メール(email)入力欄

次に、メール入力欄です。

<div style={{ marginTop: "8px" }}>
  <label>
    メール:
    <input
      type="email"
      value={email}
      onChange={(e) => setEmail(e.target.value)}
      placeholder="メールアドレスを入力してください"
    />
  </label>
</div>
JSX

ここでも、

  • value={email}
  • onChange={(e) => setEmail(e.target.value)}

という形で、 メール入力欄をControlled Componentにしています。

年齢(number)入力欄

年齢は type="number" を使います。

<div style={{ marginTop: "8px" }}>
  <label>
    年齢:
    <input
      type="number"
      value={age}
      onChange={(e) => setAge(e.target.value)}
      placeholder="年齢を入力してください"
    />
  </label>
</div>
JSX

ここでのポイントは、

  • age を文字列として扱っていること(useState("")
  • 必要に応じて、後で数値に変換して使うことができること

です。

重要な感覚:

入力欄は基本的に「文字列」としてStateに保存し、 数値が必要な場面で Number(age) のように変換する。

この方針にしておくと、 フォームの扱いがシンプルになります。

textareaとselectをControlled Componentで扱う

自己紹介(textarea)

textarea も、input と同じようにControlled Componentにできます。

<div style={{ marginTop: "8px" }}>
  <label>
    自己紹介:
    <textarea
      value={bio}
      onChange={(e) => setBio(e.target.value)}
      placeholder="簡単な自己紹介を書いてください"
      rows={4}
    />
  </label>
</div>
JSX

ここでも、

  • value={bio}
  • onChange={(e) => setBio(e.target.value)}

という形で、 自己紹介欄をStateに紐づけています。

ポイント:

textarea も「value+onChange」でControlled Componentになる。 特別な扱いは不要で、input と同じパターンで考えられる。

コース選択(select)

最後に、select をControlled Componentとして扱います。

<div style={{ marginTop: "8px" }}>
  <label>
    受講コース:
    <select
      value={course}
      onChange={(e) => setCourse(e.target.value)}
    >
      <option value="react">React入門コース</option>
      <option value="js">JavaScript基礎コース</option>
      <option value="ts">TypeScriptステップアップコース</option>
    </select>
  </label>
</div>
JSX

ここでのポイントは、

  • value={course} によって、選択されている項目がState course に紐づいていること
  • onChange で選択が変わるたびに setCourse(e.target.value) を呼んでいること

です。

重要な感覚:

select も「value+onChange」でControlled Componentになる。 選択された値は、Stateとしていつでも参照できる。

複数フォームを1つのコンポーネントとしてまとめる

フォーム全体のコード(DAY 33 前半版)

ここまでの内容をすべてまとめると、 次のような「複数フォーム」のコンポーネントになります。

import { useState } from "react";

function MultiFormBasic() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState("");
  const [bio, setBio] = useState("");
  const [course, setCourse] = useState("react");

  const handleSubmit = (e) => {
    e.preventDefault();

    alert(
      `送信された情報\n` +
      `名前: ${name}\n` +
      `メール: ${email}\n` +
      `年齢: ${age}\n` +
      `自己紹介: ${bio}\n` +
      `受講コース: ${course}`
    );
  };

  return (
    <div>
      <h2>DAY 33:複数フォーム(前半)― 基本フォーム</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            メール:
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="メールアドレスを入力してください"
            />
          </label>
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            年齢:
            <input
              type="number"
              value={age}
              onChange={(e) => setAge(e.target.value)}
              placeholder="年齢を入力してください"
            />
          </label>
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            自己紹介:
            <textarea
              value={bio}
              onChange={(e) => setBio(e.target.value)}
              placeholder="簡単な自己紹介を書いてください"
              rows={4}
            />
          </label>
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            受講コース:
            <select
              value={course}
              onChange={(e) => setCourse(e.target.value)}
            >
              <option value="react">React入門コース</option>
              <option value="js">JavaScript基礎コース</option>
              <option value="ts">TypeScriptステップアップコース</option>
            </select>
          </label>
        </div>

        <div style={{ marginTop: "12px" }}>
          <button type="submit">送信</button>
        </div>
      </form>

      <hr />

      <div>
        <h3>現在の入力内容</h3>
        <p>名前:{name}</p>
        <p>メール:{email}</p>
        <p>年齢:{age}</p>
        <p>自己紹介:{bio}</p>
        <p>受講コース:{course}</p>
      </div>
    </div>
  );
}

export default MultiFormBasic;
JSX

このコンポーネントは、

  • 5つの入力項目(名前・メール・年齢・textarea・select)を すべてControlled Componentとして扱っています。
  • それぞれが独立したStateを持ち、 value+onChange で入力内容と同期しています。
  • handleSubmit で、すべてのStateから値を取り出してまとめて扱っています。

ここで大事なのは、「項目が増えてもやっていることは同じ」という感覚です。

DAY 33 前半のまとめ ― 「複数フォームでも型は同じ」

DAY 33前半で、自分の中に定着させてほしいのは次のポイントです。

  • 複数フォームでも、基本は 項目ごとにStateを持ち、value+onChangeでControlled Componentにする という形で考えられること。
  • 名前・メール・年齢・textarea・selectのどれも、 「State → value → onChange → State更新 →画面に反映」という同じ流れで扱えること。
  • selecttextarea も特別扱いではなく、 input と同じパターンでControlled Componentとして設計できること。
  • handleSubmit で、複数のStateから値をまとめて取り出すことで、 「フォーム全体としての送信処理」を自然に書けること。

この「複数項目でも型は同じ」という感覚が身につくと、 この先のフォーム・コンポーネント設計(バリデーション・エラー表示・ネストしたフォームなど)が、 すべてこの基本形の延長として、落ち着いて組み立てられるようになっていきます。


DAY 33:複数フォーム 中盤 ― 「複数項目×Controlled Component」を設計目線で整理する

DAY 33の中盤では、前半で作った

  • 名前
  • メール
  • 年齢
  • textarea(自己紹介)
  • select(コース選択)

を、「設計目線」で整理し直すことを目標にします。

単に「動くフォーム」から一歩進んで、

  • どの項目がどのStateと対応しているか
  • どの入力がどのonChangeで処理されているか
  • どうやって「フォーム全体」として扱うか

を、意識して見ていきます。

複数フォームを「1つのデータセット」として捉える

項目ごとのStateを「1枚のカード」としてイメージする

前半では、次のように項目ごとにStateを持ちました。

const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState("");
const [bio, setBio] = useState("");
const [course, setCourse] = useState("react");
JSX

中盤では、これを「バラバラの変数」ではなく、

1人のユーザー情報カード

としてイメージしてみてください。

  • 名前 → ユーザーの名前
  • メール → 連絡先
  • 年齢 → プロフィール情報
  • 自己紹介 → テキストの説明
  • コース選択 → 受講プラン

この「1枚のカード」を、フォームで編集しているイメージです。

重要な感覚:

複数フォームは、「複数のStateを持つコンポーネント」ではなく、 「1つの意味のあるデータ(ユーザー情報)を編集するUI」として捉える。

この視点があると、 フォーム設計がぐっと整理されて見えてきます。

各項目とState・onChangeの対応関係をはっきりさせる

名前入力欄:text+State+onChange

<label>
  名前:
  <input
    type="text"
    value={name}
    onChange={(e) => setName(e.target.value)}
    placeholder="名前を入力してください"
  />
</label>
JSX
  • State: name
  • 更新関数: setName
  • 入力欄: input type="text"
  • 流れ: ユーザー入力 → onChange → setNamename更新 → 画面に反映

メール入力欄:email+State+onChange

<label>
  メール:
  <input
    type="email"
    value={email}
    onChange={(e) => setEmail(e.target.value)}
    placeholder="メールアドレスを入力してください"
  />
</label>
JSX
  • State: email
  • 更新関数: setEmail
  • 入力欄: input type="email"
  • 流れ: ユーザー入力 → onChange → setEmailemail更新 → 画面に反映

年齢入力欄:number+State+onChange

<label>
  年齢:
  <input
    type="number"
    value={age}
    onChange={(e) => setAge(e.target.value)}
    placeholder="年齢を入力してください"
  />
</label>
JSX
  • State: age(文字列として保持)
  • 更新関数: setAge
  • 入力欄: input type="number"
  • 流れ: ユーザー入力 → onChange → setAgeage更新 → 画面に反映

ここでのポイント:

入力欄の値は基本「文字列」としてStateに保存し、 数値が必要な場面で Number(age) に変換する。

自己紹介:textarea+State+onChange

<label>
  自己紹介:
  <textarea
    value={bio}
    onChange={(e) => setBio(e.target.value)}
    placeholder="簡単な自己紹介を書いてください"
    rows={4}
  />
</label>
JSX
  • State: bio
  • 更新関数: setBio
  • 入力欄: textarea
  • 流れ: ユーザー入力 → onChange → setBiobio更新 → 画面に反映

コース選択:select+State+onChange

<label>
  受講コース:
  <select
    value={course}
    onChange={(e) => setCourse(e.target.value)}
  >
    <option value="react">React入門コース</option>
    <option value="js">JavaScript基礎コース</option>
    <option value="ts">TypeScriptステップアップコース</option>
  </select>
</label>
JSX
  • State: course
  • 更新関数: setCourse
  • 入力欄: selectoption
  • 流れ: ユーザー選択 → onChange → setCoursecourse更新 → 画面に反映

重要な感覚:

input だけでなく、textareaselect も 「value+onChange+State」の同じパターンで扱える。

複数フォームを「Controlled Componentの集合」として見る

フォーム全体を分解してみる

中盤では、フォーム全体を「Controlled Componentの集合」として見てみます。

<form onSubmit={handleSubmit}>
  {/* 名前 */}
  {/* メール */}
  {/* 年齢 */}
  {/* 自己紹介 */}
  {/* 受講コース */}
  {/* 送信ボタン */}
</form>
JSX

それぞれの入力欄は、

  • 自分専用のStateを持ち
  • 自分専用のonChangeでStateを更新し
  • 自分専用のvalueで表示を制御している

という意味で、 独立したControlled Component です。

それがフォームの中に並んでいるイメージです。

ポイント:

「複数フォーム=1つの巨大なコンポーネント」ではなく、 「複数のControlled Componentが並んだフォーム」として捉える。

この視点があると、 項目が増えても「同じパターンの繰り返し」として落ち着いて扱えます。

フォーム全体の送信処理を設計する

handleSubmitで「1枚のカード」として扱う

前半で書いた送信処理を、設計目線で見直します。

const handleSubmit = (e) => {
  e.preventDefault();

  alert(
    `送信された情報\n` +
    `名前: ${name}\n` +
    `メール: ${email}\n` +
    `年齢: ${age}\n` +
    `自己紹介: ${bio}\n` +
    `受講コース: ${course}`
  );
};
JSX

ここで起きていることは、

  • e.preventDefault() でページリロードを防いでいる
  • 各Stateから値を取り出して、1つのメッセージとしてまとめている

という流れです。

重要な感覚:

フォーム送信時には、「複数のState」を 「1つの意味のあるデータ(ユーザー情報)」としてまとめて扱う。

この考え方は、 APIに送信するときにもそのまま使えます。

例えば、次のような形です。

const payload = {
  name,
  email,
  age: Number(age),
  bio,
  course,
};

// fetch("/api/register", { method: "POST", body: JSON.stringify(payload) });
JSX

中盤版テンプレート:複数フォーム+軽いチェック

DAY 33中盤では、 前半のフォームに少しだけ「チェック」を足したテンプレートを示します。

import { useState } from "react";

function MultiFormMid() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState("");
  const [bio, setBio] = useState("");
  const [course, setCourse] = useState("react");

  const handleSubmit = (e) => {
    e.preventDefault();

    if (name.trim() === "" || email.trim() === "") {
      alert("名前とメールは必須です。");
      return;
    }

    alert(
      `送信された情報\n` +
      `名前: ${name}\n` +
      `メール: ${email}\n` +
      `年齢: ${age}\n` +
      `自己紹介: ${bio}\n` +
      `受講コース: ${course}`
    );
  };

  const isNameEmpty = name.trim() === "";
  const isEmailEmpty = email.trim() === "";

  return (
    <div>
      <h2>DAY 33:複数フォーム(中盤)― 基本フォーム+軽いチェック</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {isNameEmpty && (
            <p style={{ color: "red" }}>名前は必須です。</p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            メール:
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="メールアドレスを入力してください"
            />
          </label>
          {isEmailEmpty && (
            <p style={{ color: "red" }}>メールは必須です。</p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            年齢:
            <input
              type="number"
              value={age}
              onChange={(e) => setAge(e.target.value)}
              placeholder="年齢を入力してください"
            />
          </label>
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            自己紹介:
            <textarea
              value={bio}
              onChange={(e) => setBio(e.target.value)}
              placeholder="簡単な自己紹介を書いてください"
              rows={4}
            />
          </label>
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            受講コース:
            <select
              value={course}
              onChange={(e) => setCourse(e.target.value)}
            >
              <option value="react">React入門コース</option>
              <option value="js">JavaScript基礎コース</option>
              <option value="ts">TypeScriptステップアップコース</option>
            </select>
          </label>
        </div>

        <div style={{ marginTop: "12px" }}>
          <button type="submit">送信</button>
        </div>
      </form>

      <hr />

      <div>
        <h3>現在の入力内容</h3>
        <p>名前:{name}</p>
        <p>メール:{email}</p>
        <p>年齢:{age}</p>
        <p>自己紹介:{bio}</p>
        <p>受講コース:{course}</p>
      </div>
    </div>
  );
}

export default MultiFormMid;
JSX

このテンプレートでは、

  • 各項目がControlled Componentとして設計されている
  • 名前とメールに対して「必須チェック」を入れている
  • フォーム全体を「1つのユーザー情報」として送信している

という形で、 複数フォームを設計目線で扱う練習になっています。

DAY 33 中盤のまとめ ― 「複数フォーム=複数のControlled Component+1枚のカード」

DAY 33中盤で、自分の中に定着させてほしいのは次のポイントです。

  • 複数フォームは、 複数のControlled Component(名前・メール・年齢・textarea・select)が並んだものとして捉えられること。
  • 各項目は、 自分専用のState・value・onChangeを持ち、 「入力→State→画面」の同じ流れで動いていること。
  • フォーム送信時には、 複数のStateを「1枚の意味のあるデータ(ユーザー情報)」としてまとめて扱うことができること。
  • 名前・メール・年齢・textarea・selectという5つの項目を通して、 「項目が増えてもやっていることは同じ」という感覚が持てること。

この感覚が身につくと、 この先のフォーム・コンポーネント設計(バリデーション強化、エラー管理、オブジェクトStateへの移行など)が、 すべてこの「複数のControlled Component+1枚のカード」という枠組みの中で、 落ち着いて考えられるようになっていきます。


DAY 33:複数フォーム 後半 ― 「実戦で使える複数フォーム」に仕上げる

DAY 33の後半では、前半・中盤で作ってきた

  • 名前
  • メール
  • 年齢
  • textarea(自己紹介)
  • select(コース選択)

を使った複数フォームを、「実戦で使えるフォーム」レベルに仕上げることを目標にします。

ここで意識したいのは、

  • 入力チェック(必須・形式・範囲)をどう組み込むか
  • エラーメッセージをどう表示するか
  • 「送信してよい状態か」をどう判定するか

という、現場でよく出てくるフォーム設計の考え方です。

複数フォームに「最低限のバリデーション」を組み込む

まずは何をチェックするかを決める

今回のフォームでは、次のようなルールを決めます。

  • 名前:必須(空はNG)
  • メール:必須+「@」を含む簡易チェック
  • 年齢:任意だが、入力されている場合は 0〜120 の範囲に収める
  • 自己紹介:任意(今回はチェックしない)
  • コース選択:必須(selectの値が空になるケースは避ける)

このルールを、Stateと組み合わせて実装していきます。

エラー状態を「派生情報」として扱う

Stateそのものではなく「Stateから計算される値」としてエラーを考える

フォームのエラー状態は、 「別のState」として持つこともできますが、 まずは 既存のStateから計算される値 として扱う方がシンプルです。

例えば、次のように書きます。

const isNameEmpty = name.trim() === "";
const isEmailEmpty = email.trim() === "";
const isEmailInvalidFormat =
  !isEmailEmpty && !email.includes("@");

const hasAgeValue = age.trim() !== "";
const ageNumber = hasAgeValue ? Number(age) : null;
const isAgeOutOfRange =
  hasAgeValue && (ageNumber < 0 || ageNumber > 120);

const isFormValid =
  !isNameEmpty &&
  !isEmailEmpty &&
  !isEmailInvalidFormat &&
  !isAgeOutOfRange;
JSX

ここでのポイントは、

  • エラー状態を「Stateそのもの」ではなく、「Stateから導かれる派生情報」として扱っていること
  • isFormValid を使って、「送信してよいかどうか」を一括で判定していること

です。

重要な感覚:

入力値(State)と、 その入力値から計算される「エラー状態」や「有効/無効状態」は、 別々に考えると整理しやすくなります。

エラーメッセージをフォームに自然に組み込む

項目ごとにエラーメッセージを表示する

先ほどの派生情報を使って、 各項目の下にエラーメッセージを表示してみます。

<div>
  <label>
    名前:
    <input
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="名前を入力してください"
    />
  </label>
  {isNameEmpty && (
    <p style={{ color: "red" }}>名前は必須です。</p>
  )}
</div>

<div style={{ marginTop: "8px" }}>
  <label>
    メール:
    <input
      type="email"
      value={email}
      onChange={(e) => setEmail(e.target.value)}
      placeholder="メールアドレスを入力してください"
    />
  </label>
  {isEmailEmpty && (
    <p style={{ color: "red" }}>メールは必須です。</p>
  )}
  {isEmailInvalidFormat && (
    <p style={{ color: "red" }}>メールアドレスの形式が正しくありません。</p>
  )}
</div>

<div style={{ marginTop: "8px" }}>
  <label>
    年齢:
    <input
      type="number"
      value={age}
      onChange={(e) => setAge(e.target.value)}
      placeholder="年齢を入力してください(任意)"
    />
  </label>
  {isAgeOutOfRange && (
    <p style={{ color: "red" }}>
      年齢は0〜120の範囲で入力してください。
    </p>
  )}
</div>
JSX

ここで起きていることは、

  • 入力値に応じてエラー状態が変わる
  • エラー状態に応じてメッセージが表示/非表示になる

という流れです。

ポイント:

Controlled Componentでフォームを作ると、 「入力値 → エラー状態 → メッセージ表示」という流れを すべてReactの中で完結させることができます。

送信処理に「バリデーションの最終チェック」を入れる

handleSubmitで「最後の門番」をする

フォーム送信時には、 isFormValid を使って最終チェックを行います。

const handleSubmit = (e) => {
  e.preventDefault();

  if (!isFormValid) {
    alert("入力内容に問題があります。各項目を確認してください。");
    return;
  }

  alert(
    `送信された情報\n` +
    `名前: ${name}\n` +
    `メール: ${email}\n` +
    `年齢: ${age}\n` +
    `自己紹介: ${bio}\n` +
    `受講コース: ${course}`
  );

  // 送信後にフォームをリセットする例
  setName("");
  setEmail("");
  setAge("");
  setBio("");
  setCourse("react");
};
JSX

ここでのポイントは、

  • isFormValid を使って、「フォーム全体として有効かどうか」を一括で判定していること
  • 無効な場合は送信処理を止めて、ユーザーに確認を促していること
  • 有効な場合は送信し、その後フォームを初期状態に戻していること

です。

重要な感覚:

「項目ごとのエラー表示」と「フォーム全体の有効/無効判定」は、 同じ派生情報(isNameEmpty など)から計算できる。

DAY 33 後半版:複数フォーム+バリデーション付きテンプレート

ここまでの内容をすべてまとめた、 DAY 33後半の複数フォームテンプレートを示します。

import { useState } from "react";

function MultiFormFinal() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState("");
  const [bio, setBio] = useState("");
  const [course, setCourse] = useState("react");

  // 派生情報(エラー状態など)
  const isNameEmpty = name.trim() === "";
  const isEmailEmpty = email.trim() === "";
  const isEmailInvalidFormat =
    !isEmailEmpty && !email.includes("@");

  const hasAgeValue = age.trim() !== "";
  const ageNumber = hasAgeValue ? Number(age) : null;
  const isAgeOutOfRange =
    hasAgeValue && (ageNumber < 0 || ageNumber > 120);

  const isFormValid =
    !isNameEmpty &&
    !isEmailEmpty &&
    !isEmailInvalidFormat &&
    !isAgeOutOfRange;

  const handleSubmit = (e) => {
    e.preventDefault();

    if (!isFormValid) {
      alert("入力内容に問題があります。各項目を確認してください。");
      return;
    }

    alert(
      `送信された情報\n` +
      `名前: ${name}\n` +
      `メール: ${email}\n` +
      `年齢: ${age}\n` +
      `自己紹介: ${bio}\n` +
      `受講コース: ${course}`
    );

    // フォームのリセット
    setName("");
    setEmail("");
    setAge("");
    setBio("");
    setCourse("react");
  };

  return (
    <div>
      <h2>DAY 33:複数フォーム(後半)― バリデーション付きフォーム</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {isNameEmpty && (
            <p style={{ color: "red" }}>名前は必須です。</p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            メール:
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="メールアドレスを入力してください"
            />
          </label>
          {isEmailEmpty && (
            <p style={{ color: "red" }}>メールは必須です。</p>
          )}
          {isEmailInvalidFormat && (
            <p style={{ color: "red" }}>
              メールアドレスの形式が正しくありません。
            </p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            年齢:
            <input
              type="number"
              value={age}
              onChange={(e) => setAge(e.target.value)}
              placeholder="年齢を入力してください(任意)"
            />
          </label>
          {isAgeOutOfRange && (
            <p style={{ color: "red" }}>
              年齢は0〜120の範囲で入力してください。
            </p>
          )}
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            自己紹介:
            <textarea
              value={bio}
              onChange={(e) => setBio(e.target.value)}
              placeholder="簡単な自己紹介を書いてください"
              rows={4}
            />
          </label>
        </div>

        <div style={{ marginTop: "8px" }}>
          <label>
            受講コース:
            <select
              value={course}
              onChange={(e) => setCourse(e.target.value)}
            >
              <option value="react">React入門コース</option>
              <option value="js">JavaScript基礎コース</option>
              <option value="ts">TypeScriptステップアップコース</option>
            </select>
          </label>
        </div>

        <div style={{ marginTop: "12px" }}>
          <button type="submit" disabled={!isFormValid}>
            送信
          </button>
        </div>
      </form>

      <hr />

      <div>
        <h3>現在の入力内容</h3>
        <p>名前:{name}</p>
        <p>メール:{email}</p>
        <p>年齢:{age}</p>
        <p>自己紹介:{bio}</p>
        <p>受講コース:{course}</p>
        <p>フォーム有効か:{isFormValid ? "はい" : "いいえ"}</p>
      </div>
    </div>
  );
}

export default MultiFormFinal;
JSX

DAY 33 後半のまとめ ― 「複数フォーム+バリデーション」が実戦の基本形

DAY 33後半で、自分の中に残してほしいのは次のポイントです。

  • 複数フォームでも、 名前・メール・年齢・textarea・selectのすべてがControlled Componentとして動いていること。
  • エラー状態は、 入力値(State)から計算される「派生情報」として扱うと、 項目ごとのメッセージ表示やフォーム全体の有効/無効判定を整理しやすいこと。
  • isFormValid のような「フォーム全体の有効判定」を用意しておくと、 送信ボタンの disabledhandleSubmit の最終チェックに使えて便利なこと。
  • このテンプレートが、 実際のアプリで使う複数フォーム(ユーザー登録・プロフィール編集・問い合わせフォームなど)の 基本形としてそのまま応用できること。

この「複数フォーム+バリデーション付きControlled Component」の型が身につくと、 この先のフォーム・コンポーネント設計(オブジェクトStateへの移行、エラー管理の共通化、API連携など)が、 すべてこの基本形の延長として、自然に組み立てられるようになっていきます。

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