- DAY 35:バリデーション 前半 ― 「入力を信じない」ための基本4セットを身につける
- バリデーションの全体像を言葉でつかむ
- 例題フォームの準備 ― 名前+年齢フォーム
- 必須チェック ― 「空はダメ」をコードで表現する
- 文字数チェック ― 「長すぎる名前」を防ぐ
- 数値チェック ― 「年齢が正しい数値か」を確認する
- エラーメッセージ ― 「何がダメなのか」をはっきり伝える
- DAY 35 前半版テンプレート ― 必須+文字数+数値+エラーメッセージ
- DAY 35 前半のまとめ ― 「4つの基本チェック」を自分の手で書けるようになる
- DAY 35:バリデーション 中盤 ― 「フィールドごとのエラー」を設計してみる
- フィールドごとのエラーを持つという発想
- 例題フォームのベースを作る(名前+年齢+フィールドエラー)
- 必須チェックと文字数チェックを「名前専用のバリデーション」としてまとめる
- 数値チェックを「年齢専用のバリデーション」としてまとめる
- エラーメッセージをフィールドのすぐ下に表示する意味
- DAY 35 中盤版テンプレート ― フィールドごとのバリデーション付きフォーム
- DAY 35 中盤のまとめ ― 「フィールドごとのバリデーション設計」ができるようになる
- DAY 35:バリデーション 後半 ― 「実戦で使えるバリデーションの型」に仕上げる
- ユーザー登録フォームをバリデーション付きで設計する
- バリデーション付きユーザー登録フォームの骨組み
- フィールドごとのバリデーション関数を作る
- handleSubmitの中でバリデーションを組み合わせる
- エラーメッセージの表示位置と内容を意識する
- DAY 35 後半版テンプレート ― 実戦寄りバリデーション付きユーザー登録フォーム
- 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>
)}
JSXhandleSubmit の中では、 条件を満たさないたびに 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;
JSXDAY 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;
JSXDAY 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ここでの流れは、
- エラーStateをクリアする
- 各フィールドのバリデーション関数を呼び出す
- 結果をそれぞれのエラーStateに反映する
- どれか1つでもエラーがあれば送信処理を止める
- すべて問題なければ送信成功とする
という形になっています。
重要な感覚:
「バリデーション関数はルールを知っている人」 「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;
JSXDAY 35 後半のまとめ ― 「バリデーションの型」を自分のものにする
DAY 35後半で定着させてほしいポイントは次の通りです。
- 必須チェック・文字数・数値チェック・エラーメッセージを、フィールドごとの関数として整理できること。
- バリデーション関数は「ルールを知っている人」、
handleSubmitは「結果を受け取って送信するかどうかを決める人」として役割分担すると、コードがすっきりすること。 - エラーメッセージは、フィールドのすぐ下に表示し、「何がダメで、どう直せばよいか」を具体的に伝えることが大切であること。
この型が手に馴染んでくると、 より複雑なフォーム(複数フィールド・複数エラー・リアルタイムバリデーション・ライブラリ活用など)も、 落ち着いて設計できるようになっていきます。
