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

React 60日で身につけるReact
スポンサーリンク
スポンサーリンク
  1. DAY 35:バリデーション 前半 ― 「入力を信じない」ための基本4セットを身につける
  2. バリデーションの全体像を言葉でつかむ
    1. 「入力をそのまま信じない」という姿勢
  3. 例題フォームの準備 ― 名前+年齢フォーム
    1. シンプルなフォームを題材にする
  4. 必須チェック ― 「空はダメ」をコードで表現する
    1. 名前を必須にする
  5. 文字数チェック ― 「長すぎる名前」を防ぐ
    1. 最大文字数を決める
  6. 数値チェック ― 「年齢が正しい数値か」を確認する
    1. 年齢を任意+範囲付きの数値として扱う
  7. エラーメッセージ ― 「何がダメなのか」をはっきり伝える
    1. 1つのエラーメッセージStateでシンプルに始める
  8. DAY 35 前半版テンプレート ― 必須+文字数+数値+エラーメッセージ
  9. DAY 35 前半のまとめ ― 「4つの基本チェック」を自分の手で書けるようになる
  10. DAY 35:バリデーション 中盤 ― 「フィールドごとのエラー」を設計してみる
  11. フィールドごとのエラーを持つという発想
    1. 「フォーム全体のエラー」から「項目ごとのエラー」へ
  12. 例題フォームのベースを作る(名前+年齢+フィールドエラー)
    1. Stateの構造を少しだけ拡張する
  13. 必須チェックと文字数チェックを「名前専用のバリデーション」としてまとめる
    1. 名前のバリデーションロジックを1箇所に集約する
  14. 数値チェックを「年齢専用のバリデーション」としてまとめる
    1. 年齢のバリデーション関数を作る
  15. エラーメッセージをフィールドのすぐ下に表示する意味
    1. ユーザーに「どこが問題か」を即座に伝える
  16. DAY 35 中盤版テンプレート ― フィールドごとのバリデーション付きフォーム
  17. DAY 35 中盤のまとめ ― 「フィールドごとのバリデーション設計」ができるようになる
  18. DAY 35:バリデーション 後半 ― 「実戦で使えるバリデーションの型」に仕上げる
  19. ユーザー登録フォームをバリデーション付きで設計する
    1. 対象フィールドとルールを言葉で整理する
  20. バリデーション付きユーザー登録フォームの骨組み
    1. State構造と基本コンポーネント
  21. フィールドごとのバリデーション関数を作る
    1. 名前:必須+文字数
    2. メール:必須+簡易形式チェック
    3. 年齢:任意+数値+範囲チェック
  22. handleSubmitの中でバリデーションを組み合わせる
    1. 全フィールドをチェックしてから送信する
  23. エラーメッセージの表示位置と内容を意識する
    1. 「どこが」「なぜ」ダメなのかを伝える
  24. DAY 35 後半版テンプレート ― 実戦寄りバリデーション付きユーザー登録フォーム
  25. DAY 35 後半のまとめ ― 「バリデーションの型」を自分のものにする

DAY 35:バリデーション 前半 ― 「入力を信じない」ための基本4セットを身につける

DAY 35では、フォーム・コンポーネント設計の中でもとても重要なテーマであるバリデーションを扱います。 前半のゴールは、次の4つを「自分で組み立てられる」レベルにすることです。

  • 必須チェック
  • 文字数チェック
  • 数値チェック
  • エラーメッセージの表示

ここでは、シンプルな「ユーザー情報フォーム」を題材に、 ステップバイステップでかみ砕いて説明していきます。

バリデーションの全体像を言葉でつかむ

「入力をそのまま信じない」という姿勢

バリデーションとは、ざっくり言うと、

ユーザーが入力した値が「アプリが期待する条件」を満たしているかどうかをチェックすること

です。

例えば、

  • 名前が空ではないか(必須チェック)
  • 名前が長すぎないか(文字数チェック)
  • 年齢が数値として正しいか(数値チェック)
  • 条件を満たさないときに、ユーザーに何が問題かを伝える(エラーメッセージ)

といったことを行います。

重要な感覚:

フォーム設計では、「入力をそのまま信じない」ことが基本です。 信じる前に、必ずチェックする癖をつけていきます。

例題フォームの準備 ― 名前+年齢フォーム

シンプルなフォームを題材にする

まずは、次のようなフォームを用意します。

  • 名前(必須・最大20文字)
  • 年齢(任意だが、入力されている場合は 0〜120 の数値)

これをReactコンポーネントとして書くと、次のようになります。

import { useState } from "react";

function ValidationFormBasic() {
  const [name, setName] = useState("");
  const [age, setAge] = useState("");

  const [errorMessage, setErrorMessage] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    // ここにバリデーション処理を追加していきます
  };

  return (
    <div>
      <h2>DAY 35:バリデーション前半 ― 名前+年齢フォーム</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="number"
              value={age}
              onChange={(e) => setAge(e.target.value)}
              placeholder="年齢を入力してください(任意)"
            />
          </label>
        </div>

        {errorMessage && (
          <p style={{ color: "red", marginTop: "8px" }}>{errorMessage}</p>
        )}

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

export default ValidationFormBasic;
JSX

ここから、バリデーションの4つの基本を順番に組み込んでいきます。

必須チェック ― 「空はダメ」をコードで表現する

名前を必須にする

まずは、必須チェックです。 名前が空の場合にエラーにする処理を handleSubmit に追加します。

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

  setErrorMessage(""); // いったんエラーをクリアします。

  // 必須チェック:名前が空かどうか
  if (name.trim() === "") {
    setErrorMessage("名前は必須です。");
    return;
  }

  alert(`送信成功:名前=${name}, 年齢=${age}`);
};
JSX

ここでのポイントは、

  • name.trim() === "" で「空文字+空白だけ」のケースをまとめて判定していること
  • 条件を満たさない場合は setErrorMessage でメッセージをセットし、return で処理を止めていること
  • 条件を満たした場合のみ、次の処理(ここでは alert)に進むこと

です。

重要な感覚:

必須チェックは、「空ならエラーにして処理を止める」というシンプルなパターンで書けます。 trim() を使って、空白だけの入力も「空」とみなすのが基本です。

文字数チェック ― 「長すぎる名前」を防ぐ

最大文字数を決める

次に、文字数チェックを追加します。 ここでは「名前は最大20文字まで」としてみます。

const MAX_NAME_LENGTH = 20;

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

  setErrorMessage("");

  // 必須チェック
  if (name.trim() === "") {
    setErrorMessage("名前は必須です。");
    return;
  }

  // 文字数チェック
  if (name.length > MAX_NAME_LENGTH) {
    setErrorMessage(`名前は${MAX_NAME_LENGTH}文字以内で入力してください。`);
    return;
  }

  alert(`送信成功:名前=${name}, 年齢=${age}`);
};
JSX

ここでのポイントは、

  • MAX_NAME_LENGTH のように「ルール」を定数として切り出しておくと、後から変更しやすいこと
  • name.length で文字数をチェックしていること
  • 条件を満たさない場合は、具体的な数字を含んだエラーメッセージを表示していること

です。

重要な感覚:

文字数チェックは、「ルールを定数にしておく」と読みやすく・変更しやすくなります。 エラーメッセージには、ユーザーが何文字まで許されているかを明示すると親切です。

数値チェック ― 「年齢が正しい数値か」を確認する

年齢を任意+範囲付きの数値として扱う

次に、数値チェックを追加します。 年齢は任意ですが、入力されている場合は 0〜120 の範囲に収めることにします。

const MIN_AGE = 0;
const MAX_AGE = 120;

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

  setErrorMessage("");

  // 必須チェック(名前)
  if (name.trim() === "") {
    setErrorMessage("名前は必須です。");
    return;
  }

  // 文字数チェック(名前)
  if (name.length > MAX_NAME_LENGTH) {
    setErrorMessage(`名前は${MAX_NAME_LENGTH}文字以内で入力してください。`);
    return;
  }

  // 数値チェック(年齢)
  const hasAge = age.trim() !== "";
  if (hasAge) {
    const ageNumber = Number(age);

    if (Number.isNaN(ageNumber)) {
      setErrorMessage("年齢は数値で入力してください。");
      return;
    }

    if (ageNumber < MIN_AGE || ageNumber > MAX_AGE) {
      setErrorMessage(`年齢は${MIN_AGE}${MAX_AGE}の範囲で入力してください。`);
      return;
    }
  }

  alert(`送信成功:名前=${name}, 年齢=${age}`);
};
JSX

ここでのポイントは、

  • 年齢が空の場合は「チェックしない」(任意項目)としていること
  • Number(age) で文字列を数値に変換し、Number.isNaN で「数値として解釈できるか」を確認していること
  • 範囲チェックで「ありえない年齢」を防いでいること

です。

重要な感覚:

数値チェックでは、「入力されているか」「数値として解釈できるか」「範囲内か」の3段階で考えると整理しやすくなります。

エラーメッセージ ― 「何がダメなのか」をはっきり伝える

1つのエラーメッセージStateでシンプルに始める

この例では、errorMessage という1つのStateでエラーメッセージを管理しています。

const [errorMessage, setErrorMessage] = useState("");

{errorMessage && (
  <p style={{ color: "red", marginTop: "8px" }}>{errorMessage}</p>
)}
JSX

handleSubmit の中では、 条件を満たさないたびに setErrorMessage("...") を呼び、return で処理を止めています。

この形は、

  • 実装がシンプル
  • 「今一番問題になっていること」を1つだけ表示できる
  • 初学者にとって理解しやすい

というメリットがあります。

重要な感覚:

エラーメッセージは、「何がダメなのか」を具体的に書くことが大切です。 ユーザーが次に何をすればよいかが分かるように書きます。

DAY 35 前半版テンプレート ― 必須+文字数+数値+エラーメッセージ

ここまでの内容をすべて含んだ、 DAY 35前半のバリデーション付きフォームテンプレートをまとめます。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;
const MIN_AGE = 0;
const MAX_AGE = 120;

function ValidationFormBasic() {
  const [name, setName] = useState("");
  const [age, setAge] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

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

    setErrorMessage("");

    // 必須チェック(名前)
    if (name.trim() === "") {
      setErrorMessage("名前は必須です。");
      return;
    }

    // 文字数チェック(名前)
    if (name.length > MAX_NAME_LENGTH) {
      setErrorMessage(`名前は${MAX_NAME_LENGTH}文字以内で入力してください。`);
      return;
    }

    // 数値チェック(年齢)
    const hasAge = age.trim() !== "";
    if (hasAge) {
      const ageNumber = Number(age);

      if (Number.isNaN(ageNumber)) {
        setErrorMessage("年齢は数値で入力してください。");
        return;
      }

      if (ageNumber < MIN_AGE || ageNumber > MAX_AGE) {
        setErrorMessage(`年齢は${MIN_AGE}${MAX_AGE}の範囲で入力してください。`);
        return;
      }
    }

    alert(`送信成功:名前=${name}, 年齢=${age}`);
  };

  return (
    <div>
      <h2>DAY 35:バリデーション前半 ― 基本チェック付きフォーム</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="number"
              value={age}
              onChange={(e) => setAge(e.target.value)}
              placeholder="年齢を入力してください(任意)"
            />
          </label>
        </div>

        {errorMessage && (
          <p style={{ color: "red", marginTop: "8px" }}>{errorMessage}</p>
        )}

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

export default ValidationFormBasic;
JSX

DAY 35 前半のまとめ ― 「4つの基本チェック」を自分の手で書けるようになる

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

  • バリデーションは、「入力をそのまま信じない」ための仕組みであること。
  • 必須チェックは、trim() を使って「空+空白だけ」をまとめて判定するのが基本であること。
  • 文字数チェックは、ルールを定数にしておくと読みやすく・変更しやすくなること。
  • 数値チェックは、「入力されているか」「数値として解釈できるか」「範囲内か」の3段階で考えると整理しやすいこと。
  • エラーメッセージは、「何がダメなのか」「どう直せばよいか」が分かるように具体的に書くことが大切であること。

この4つの基本が手に馴染んでくると、 この先のバリデーション(複数項目・複数エラー・フィールドごとのメッセージ・ライブラリ活用など)が、 すべてこの土台の延長として、自然に理解できるようになっていきます。


DAY 35:バリデーション 中盤 ― 「フィールドごとのエラー」を設計してみる

DAY 35の中盤では、前半で扱った

  • 必須チェック
  • 文字数
  • 数値チェック
  • エラーメッセージ

を、もう一段整理された形で扱うことを目指します。

前半では「1つのエラーメッセージState」でフォーム全体のエラーを扱いましたが、中盤では

  • フィールドごとにエラーメッセージを持つ
  • 「どの項目が問題なのか」をはっきりさせる
  • バリデーションロジックを少しだけ整理する

というステップに進みます。

題材は引き続き「名前+年齢フォーム」ですが、 実戦で使いやすい形に近づけていきます。

フィールドごとのエラーを持つという発想

「フォーム全体のエラー」から「項目ごとのエラー」へ

前半では、次のようにフォーム全体のエラーを1つの文字列で扱いました。

const [errorMessage, setErrorMessage] = useState("");
JSX

中盤では、これをフィールドごとのエラーに分けます。

  • nameError:名前に関するエラー
  • ageError:年齢に関するエラー

というように分けることで、

  • どの項目が問題なのかが一目で分かる
  • 項目の下にそれぞれエラーメッセージを表示できる
  • バリデーションロジックを整理しやすくなる

というメリットがあります。

例題フォームのベースを作る(名前+年齢+フィールドエラー)

Stateの構造を少しだけ拡張する

まずは、フィールドごとのエラーStateを追加したフォームのベースを作ります。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;
const MIN_AGE = 0;
const MAX_AGE = 120;

function ValidationFormMid() {
  const [name, setName] = useState("");
  const [age, setAge] = useState("");

  const [nameError, setNameError] = useState("");
  const [ageError, setAgeError] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    // ここにバリデーション処理を追加していきます
  };

  return (
    <div>
      <h2>DAY 35:バリデーション中盤 ― フィールドごとのエラー付きフォーム</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

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

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

export default ValidationFormMid;
JSX

ここまでで、

  • 入力値(name, age
  • フィールドごとのエラー(nameError, ageError

という2種類のStateを持つフォームの枠組みができました。

必須チェックと文字数チェックを「名前専用のバリデーション」としてまとめる

名前のバリデーションロジックを1箇所に集約する

中盤では、バリデーションロジックを少し整理して、 フィールドごとに「専用のチェック」をまとめる形にしてみます。

まずは、名前のバリデーションです。

const validateName = (name) => {
  if (name.trim() === "") {
    return "名前は必須です。";
  }

  if (name.length > MAX_NAME_LENGTH) {
    return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
  }

  return ""; // 問題なし
};
JSX

この関数を handleSubmit の中で使います。

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

  // いったんエラーをクリア
  setNameError("");
  setAgeError("");

  // 名前のバリデーション
  const nameValidationResult = validateName(name);
  if (nameValidationResult !== "") {
    setNameError(nameValidationResult);
  }

  // 年齢のバリデーション(後で追加)
};
JSX

ここでのポイントは、

  • 名前に関するルール(必須・文字数)を validateName に集約していること
  • validateName は「エラーメッセージを返す関数」として設計していること
  • 問題がない場合は空文字を返し、問題がある場合は具体的なメッセージを返すこと

です。

重要な感覚:

フィールドごとのバリデーションを「関数」として切り出すと、 ロジックが読みやすくなり、再利用もしやすくなります。

数値チェックを「年齢専用のバリデーション」としてまとめる

年齢のバリデーション関数を作る

次に、年齢のバリデーションを関数としてまとめます。

const validateAge = (age) => {
  const trimmed = age.trim();

  // 任意項目なので、空ならOK
  if (trimmed === "") {
    return "";
  }

  const ageNumber = Number(trimmed);

  if (Number.isNaN(ageNumber)) {
    return "年齢は数値で入力してください。";
  }

  if (ageNumber < MIN_AGE || ageNumber > MAX_AGE) {
    return `年齢は${MIN_AGE}${MAX_AGE}の範囲で入力してください。`;
  }

  return ""; // 問題なし
};
JSX

これを handleSubmit の中で使います。

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

  setNameError("");
  setAgeError("");

  const nameValidationResult = validateName(name);
  const ageValidationResult = validateAge(age);

  if (nameValidationResult !== "") {
    setNameError(nameValidationResult);
  }

  if (ageValidationResult !== "") {
    setAgeError(ageValidationResult);
  }

  // どちらかにエラーがある場合は送信処理を止める
  if (nameValidationResult !== "" || ageValidationResult !== "") {
    return;
  }

  alert(`送信成功:名前=${name}, 年齢=${age}`);
};
JSX

ここでのポイントは、

  • 年齢のバリデーションロジックを validateAge に集約していること
  • 任意項目であることを関数の中で表現していること(空ならOK)
  • handleSubmit の中では、「結果を受け取ってエラーStateに反映する」だけにしていること

です。

重要な感覚:

バリデーション関数は「ルールを知っている人」、 handleSubmit は「結果を受け取ってどうするかを決める人」 という役割分担で考えると整理しやすくなります。

エラーメッセージをフィールドのすぐ下に表示する意味

ユーザーに「どこが問題か」を即座に伝える

フィールドごとのエラーStateを使うことで、 次のように項目のすぐ下にエラーメッセージを表示できます。

<div>
  <label>
    名前:
    <input
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="名前を入力してください"
    />
  </label>
  {nameError && (
    <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
  )}
</div>

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

この形のメリットは、

  • ユーザーが「どの項目に問題があるか」をすぐに理解できる
  • エラーメッセージを読んで、その場で修正しやすい
  • フォーム全体のエラーよりも、ユーザー体験が良くなる

という点です。

重要な感覚:

エラーメッセージは、「問題が起きている場所の近く」に表示すると、 ユーザーが迷わず修正できます。

DAY 35 中盤版テンプレート ― フィールドごとのバリデーション付きフォーム

ここまでの内容をすべて含んだ、 DAY 35中盤のバリデーション付きフォームテンプレートをまとめます。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;
const MIN_AGE = 0;
const MAX_AGE = 120;

function ValidationFormMid() {
  const [name, setName] = useState("");
  const [age, setAge] = useState("");

  const [nameError, setNameError] = useState("");
  const [ageError, setAgeError] = useState("");

  const validateName = (name) => {
    if (name.trim() === "") {
      return "名前は必須です。";
    }

    if (name.length > MAX_NAME_LENGTH) {
      return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
    }

    return "";
  };

  const validateAge = (age) => {
    const trimmed = age.trim();

    if (trimmed === "") {
      return ""; // 任意項目なので空はOK
    }

    const ageNumber = Number(trimmed);

    if (Number.isNaN(ageNumber)) {
      return "年齢は数値で入力してください。";
    }

    if (ageNumber < MIN_AGE || ageNumber > MAX_AGE) {
      return `年齢は${MIN_AGE}${MAX_AGE}の範囲で入力してください。`;
    }

    return "";
  };

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

    setNameError("");
    setAgeError("");

    const nameValidationResult = validateName(name);
    const ageValidationResult = validateAge(age);

    if (nameValidationResult !== "") {
      setNameError(nameValidationResult);
    }

    if (ageValidationResult !== "") {
      setAgeError(ageValidationResult);
    }

    if (nameValidationResult !== "" || ageValidationResult !== "") {
      // どちらかにエラーがある場合は送信しない
      return;
    }

    alert(`送信成功:名前=${name}, 年齢=${age}`);
  };

  return (
    <div>
      <h2>DAY 35:バリデーション中盤 ― フィールドごとのエラー付きフォーム</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

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

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

export default ValidationFormMid;
JSX

DAY 35 中盤のまとめ ― 「フィールドごとのバリデーション設計」ができるようになる

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

  • バリデーションは、フォーム全体ではなくフィールドごとに考えると整理しやすくなること。
  • 必須チェック・文字数チェック・数値チェックを、それぞれ専用の関数にまとめることで、ロジックが読みやすく・再利用しやすくなること。
  • エラーメッセージをフィールドのすぐ下に表示することで、ユーザーが「どこを直せばよいか」をすぐに理解できること。
  • handleSubmit は、「バリデーション関数の結果を受け取って、送信するかどうかを決める場所」として設計すると、役割がはっきりすること。

この「フィールドごとのバリデーション設計」の感覚が身につくと、 この先のフォーム設計(複数項目・複数エラー・リアルタイムバリデーション・ライブラリ活用など)が、 すべてこの土台の延長として、自然に理解できるようになっていきます。


DAY 35:バリデーション 後半 ― 「実戦で使えるバリデーションの型」に仕上げる

DAY 35の後半では、前半・中盤で扱った

  • 必須チェック
  • 文字数
  • 数値チェック
  • エラーメッセージ

を、「実戦でそのまま使える形」にまとめていきます。 ここでは、少し項目数を増やした「ユーザー登録フォーム」を題材にして、

  • フィールドごとのエラー
  • 複数ルールの組み合わせ
  • 送信前チェックとリアルタイムチェックの考え方

をステップバイステップで整理していきます。

ユーザー登録フォームをバリデーション付きで設計する

対象フィールドとルールを言葉で整理する

まず、バリデーション対象のフィールドとルールを言葉で整理します。

  • 名前
    • 必須
    • 最大20文字
  • メール
    • 必須
    • 「@」を含む簡易チェック
  • 年齢
    • 任意
    • 入力されている場合は数値
    • 0〜120の範囲

これをReactコンポーネントに落とし込んでいきます。

バリデーション付きユーザー登録フォームの骨組み

State構造と基本コンポーネント

まずは、入力値とフィールドごとのエラーを持つフォームの骨組みです。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;
const MIN_AGE = 0;
const MAX_AGE = 120;

function UserValidationForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState("");

  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");
  const [ageError, setAgeError] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    // ここにバリデーション+送信処理を追加していきます
  };

  return (
    <div>
      <h2>DAY 35:バリデーション後半 ― ユーザー登録フォーム</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

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

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

        <div style={{ marginTop: "12px" }}>
          <button type="submit">登録</button>
        </div>
      </form>
    </div>
  );
}

export default UserValidationForm;
JSX

ここから、各フィールドのバリデーションを関数として組み立てていきます。

フィールドごとのバリデーション関数を作る

名前:必須+文字数

const validateName = (name) => {
  if (name.trim() === "") {
    return "名前は必須です。";
  }

  if (name.length > MAX_NAME_LENGTH) {
    return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
  }

  return "";
};
JSX

メール:必須+簡易形式チェック

const validateEmail = (email) => {
  if (email.trim() === "") {
    return "メールアドレスは必須です。";
  }

  if (!email.includes("@")) {
    return "メールアドレスの形式が正しくありません。";
  }

  return "";
};
JSX

年齢:任意+数値+範囲チェック

const validateAge = (age) => {
  const trimmed = age.trim();

  if (trimmed === "") {
    return ""; // 任意項目なので空はOK
  }

  const ageNumber = Number(trimmed);

  if (Number.isNaN(ageNumber)) {
    return "年齢は数値で入力してください。";
  }

  if (ageNumber < MIN_AGE || ageNumber > MAX_AGE) {
    return `年齢は${MIN_AGE}${MAX_AGE}の範囲で入力してください。`;
  }

  return "";
};
JSX

重要なポイント:

  • 各フィールドのルールを「そのフィールド専用の関数」にまとめることで、ロジックが読みやすくなります。
  • 関数は「エラーメッセージを返す」形にしておくと、扱いやすくなります(問題なしなら空文字)。

handleSubmitの中でバリデーションを組み合わせる

全フィールドをチェックしてから送信する

次に、handleSubmit の中でこれらの関数を使います。

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

  // いったんエラーをクリア
  setNameError("");
  setEmailError("");
  setAgeError("");

  // 各フィールドのバリデーション
  const nameValidationResult = validateName(name);
  const emailValidationResult = validateEmail(email);
  const ageValidationResult = validateAge(age);

  if (nameValidationResult !== "") {
    setNameError(nameValidationResult);
  }

  if (emailValidationResult !== "") {
    setEmailError(emailValidationResult);
  }

  if (ageValidationResult !== "") {
    setAgeError(ageValidationResult);
  }

  // どれか1つでもエラーがあれば送信しない
  if (
    nameValidationResult !== "" ||
    emailValidationResult !== "" ||
    ageValidationResult !== ""
  ) {
    return;
  }

  alert(`送信成功:名前=${name}, メール=${email}, 年齢=${age}`);
};
JSX

ここでの流れは、

  1. エラーStateをクリアする
  2. 各フィールドのバリデーション関数を呼び出す
  3. 結果をそれぞれのエラーStateに反映する
  4. どれか1つでもエラーがあれば送信処理を止める
  5. すべて問題なければ送信成功とする

という形になっています。

重要な感覚:

「バリデーション関数はルールを知っている人」 「handleSubmitは結果を受け取って送信するかどうかを決める人」 という役割分担で考えると、コードが整理されます。

エラーメッセージの表示位置と内容を意識する

「どこが」「なぜ」ダメなのかを伝える

フィールドごとのエラーを、入力欄のすぐ下に表示することで、

  • ユーザーが「どの項目に問題があるか」を即座に理解できる
  • エラーメッセージを読んで、その場で修正しやすい

という状態になります。

エラーメッセージの内容は、

  • 何が問題なのか(必須・文字数・形式・範囲など)
  • どう直せばよいか(何文字以内・どの範囲など)

が分かるように書くことが大切です。

DAY 35 後半版テンプレート ― 実戦寄りバリデーション付きユーザー登録フォーム

最後に、DAY 35後半の内容をすべて含んだテンプレートをまとめます。

import { useState } from "react";

const MAX_NAME_LENGTH = 20;
const MIN_AGE = 0;
const MAX_AGE = 120;

function UserValidationForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [age, setAge] = useState("");

  const [nameError, setNameError] = useState("");
  const [emailError, setEmailError] = useState("");
  const [ageError, setAgeError] = useState("");

  const validateName = (name) => {
    if (name.trim() === "") {
      return "名前は必須です。";
    }

    if (name.length > MAX_NAME_LENGTH) {
      return `名前は${MAX_NAME_LENGTH}文字以内で入力してください。`;
    }

    return "";
  };

  const validateEmail = (email) => {
    if (email.trim() === "") {
      return "メールアドレスは必須です。";
    }

    if (!email.includes("@")) {
      return "メールアドレスの形式が正しくありません。";
    }

    return "";
  };

  const validateAge = (age) => {
    const trimmed = age.trim();

    if (trimmed === "") {
      return ""; // 任意項目なので空はOK
    }

    const ageNumber = Number(trimmed);

    if (Number.isNaN(ageNumber)) {
      return "年齢は数値で入力してください。";
    }

    if (ageNumber < MIN_AGE || ageNumber > MAX_AGE) {
      return `年齢は${MIN_AGE}${MAX_AGE}の範囲で入力してください。`;
    }

    return "";
  };

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

    setNameError("");
    setEmailError("");
    setAgeError("");

    const nameValidationResult = validateName(name);
    const emailValidationResult = validateEmail(email);
    const ageValidationResult = validateAge(age);

    if (nameValidationResult !== "") {
      setNameError(nameValidationResult);
    }

    if (emailValidationResult !== "") {
      setEmailError(emailValidationResult);
    }

    if (ageValidationResult !== "") {
      setAgeError(ageValidationResult);
    }

    if (
      nameValidationResult !== "" ||
      emailValidationResult !== "" ||
      ageValidationResult !== ""
    ) {
      return;
    }

    alert(`送信成功:名前=${name}, メール=${email}, 年齢=${age}`);
  };

  return (
    <div>
      <h2>DAY 35:バリデーション後半 ― ユーザー登録フォームテンプレート</h2>

      <form onSubmit={handleSubmit}>
        <div>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="名前を入力してください"
            />
          </label>
          {nameError && (
            <p style={{ color: "red", marginTop: "4px" }}>{nameError}</p>
          )}
        </div>

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

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

        <div style={{ marginTop: "12px" }}>
          <button type="submit">登録</button>
        </div>
      </form>
    </div>
  );
}

export default UserValidationForm;
JSX

DAY 35 後半のまとめ ― 「バリデーションの型」を自分のものにする

DAY 35後半で定着させてほしいポイントは次の通りです。

  • 必須チェック・文字数・数値チェック・エラーメッセージを、フィールドごとの関数として整理できること。
  • バリデーション関数は「ルールを知っている人」、handleSubmit は「結果を受け取って送信するかどうかを決める人」として役割分担すると、コードがすっきりすること。
  • エラーメッセージは、フィールドのすぐ下に表示し、「何がダメで、どう直せばよいか」を具体的に伝えることが大切であること。

この型が手に馴染んでくると、 より複雑なフォーム(複数フィールド・複数エラー・リアルタイムバリデーション・ライブラリ活用など)も、 落ち着いて設計できるようになっていきます。

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