[React #06] フォームとバリデーション

1. フォームの取り扱い

1-1. ユーザー入力を受け取るフォームの作成

Reactでは、フォーム入力をコンポーネント内部で 状態管理(State Management) することが基本です。 入力内容をリアルタイムに追跡し、検証・送信・クリアなどを自在に制御できます。

Controlled Component とは

フォームの各入力フィールドをReactのstateで管理する仕組みです。 value属性とonChangeイベントを組み合わせて、入力値をコンポーネントの状態に同期させます。

import React, { useState } from 'react';

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });

  // 入力時のイベント
  const handleChange = e => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  // 送信時のイベント
  const handleSubmit = e => {
    e.preventDefault();
    console.log('送信データ:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        名前
        <input
          type="text"
          name="name"
          value={formData.name}
          onChange={handleChange}
        />
      </label>

      <label>
        メールアドレス
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
      </label>

      <label>
        お問い合わせ内容
        <textarea
          name="message"
          value={formData.message}
          onChange={handleChange}
        />
      </label>

      <button type="submit">送信</button>
    </form>
  );
}

export default ContactForm;

このように onChange イベントで入力値を state に反映させ、valuestate をバインドすることで、 Reactの内部状態とフォームの入力内容が常に同期される構造になります。

1-2. Uncontrolled Component(非制御コンポーネント)

フォーム入力をReactのstateで管理せず、DOM要素そのものの値を直接参照する方式です。 簡単なフォームや外部ライブラリと組み合わせる場合に有効です。

import React, { useRef } from 'react';

function SimpleForm() {
  const inputRef = useRef();

  const handleSubmit = e => {
    e.preventDefault();
    alert(`入力された名前: ${inputRef.current.value}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={inputRef} placeholder="名前を入力" />
      <button type="submit">送信</button>
    </form>
  );
}

useRefを使うことでDOM要素を直接参照できるため、 フォーム送信時にのみ値を取り出したいケースなどで便利です。

1-3. Controlled vs Uncontrolled の比較

項目 Controlled Component Uncontrolled Component
状態管理 Reactのstateで管理 DOMが保持
値の取得 stateから取得 ref.current.valueで取得
バリデーション 容易(リアルタイムで可能) 送信時のみが基本
コード量 やや多い 少ないが柔軟性に欠ける
主な用途 多段階フォーム、入力検証 簡易フォーム、外部フォーム統合

Reactでは基本的に Controlled Component を推奨しますが、 ファイルアップロードや既存ライブラリと統合する場合は Uncontrolled を併用するケースもあります。


1-4. onSubmitイベントでの送信制御

Reactでは、フォーム送信時のデフォルト動作(ページリロード)を防ぎ、 独自処理を行うのが一般的です。

const handleSubmit = e => {
  e.preventDefault();  // ← リロード防止
  console.log('フォーム送信:', formData);
};

preventDefault() を忘れるとページがリロードされ、入力内容が消えてしまいます。 SPA(Single Page Application)では必ず明示的に送信動作を制御します。


1-5. 入力イベントの扱い方まとめ

イベント 主な用途 発火タイミング
onChange 入力値の変化を検知 値が変更されるたび
onBlur フィールドからフォーカスが外れた時 バリデーション開始などに活用
onFocus 入力欄にフォーカスが当たった時 UI強調表示など
onSubmit フォーム送信時 データ送信・検証など

フォームのUXを良くするには、「どのタイミングで検証を行うか」「いつエラーを出すか」を明確に決めることが重要です。 特に、リアルタイム検証を行う場合は入力中のラグやストレスを避けるため、 onBlurや送信時のみのチェックと組み合わせる設計が好まれます。

2. エラーメッセージの表示と管理

2-1. バリデーションの目的

バリデーションとは、ユーザーが入力した内容が「期待する条件に合致しているか」を検証する仕組みです。 これにより、不正なデータ送信を防ぎ、ユーザーのミスをその場で知らせることができます。

たとえば、以下のようなケースが代表的です。

チェック内容
必須項目 名前・メールアドレスなどが空欄でないか
形式チェック メールアドレスや電話番号が正しい形式か
範囲・長さ 文字数や数値が範囲内か
一致確認 パスワードと確認用パスワードが同じか

バリデーションは、クライアント側(React)と サーバ側 の両方で行うことが基本です。 フロント側はユーザー体験を向上させ、サーバ側はセキュリティを守るために必要です。


2-2. シンプルな手動バリデーション

Reactでは、フォーム送信時に handleSubmit 内でバリデーション関数を呼び出すのが一般的です。 以下は、メールアドレスとパスワードをチェックする例です。

import React, { useState } from 'react';

function LoginForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState({});

  const handleChange = e => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  // バリデーション関数
  const validate = data => {
    const newErrors = {};
    const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

    if (!data.email.trim()) {
      newErrors.email = 'メールアドレスは必須です。';
    } else if (!emailPattern.test(data.email)) {
      newErrors.email = 'メールアドレスの形式が正しくありません。';
    }

    if (!data.password.trim()) {
      newErrors.password = 'パスワードは必須です。';
    } else if (data.password.length < 6) {
      newErrors.password = 'パスワードは6文字以上にしてください。';
    }

    return newErrors;
  };

  const handleSubmit = e => {
    e.preventDefault();
    const validationErrors = validate(formData);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    console.log('送信データ:', formData);
    alert('ログイン成功!');
  };

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label>メールアドレス</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
      </div>

      <div>
        <label>パスワード</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
      </div>

      <button type="submit">ログイン</button>
    </form>
  );
}

このように、validate()関数で入力値をチェックし、問題がある場合はerrorsオブジェクトに格納してUIに反映します。


2-3. リアルタイムバリデーション(onBlur)

「送信ボタンを押す前にエラーを出したい」という場合、onBlur(フォーカスが外れた時)にチェックを行います。

const handleBlur = e => {
  const { name, value } = e.target;
  const fieldError = validate({ [name]: value });
  setErrors(prev => ({ ...prev, ...fieldError }));
};

これにより、ユーザーが入力欄からフォーカスを外した瞬間に、その項目のみ検証できます。 リアルタイムチェックを行いすぎると入力途中でエラーが表示されて煩わしくなるため、 UXを考慮してタイミングを設計するのがポイントです。


2-4. 視覚的フィードバックの改善

フォームバリデーションでは、「何が間違っているか」を即座にわかりやすく伝えることが大切です。 以下のような視覚的工夫が有効です。

  • エラー時は枠線や背景色を変更
  • 成功時は緑色などで「OK」を明示
  • アイコンを付けてアクセシブルに(例:❌ / ✅)
input.error {
  border: 2px solid #ff4d4f;
  background: #fff5f5;
}
input.valid {
  border: 2px solid #52c41a;
  background: #f6ffed;
}
<input
  type="email"
  name="email"
  className={errors.email ? "error" : "valid"}
  value={formData.email}
  onChange={handleChange}
/>

このように、見た目の変化でフィードバックを即時に伝えると、エラーがストレスになりにくくなります。


2-5. バリデーション設計の考え方

  1. ルールを整理する どの項目にどんな条件が必要かを一覧化しておくと、後でスキーマ化(Yupなど)しやすい。

  2. エラーメッセージは明確に 「正しくありません」ではなく「6文字以上にしてください」「メール形式を確認してください」など、行動を促す。

  3. 入力補助を同時に設計 例:パスワード強度メーター、文字数カウンターなど。

  4. 多言語対応を意識する i18nでエラーメッセージを切り替え可能にしておくと拡張性が高い。

  5. パフォーマンスを考慮する 毎回全項目を再検証すると処理が重くなる。部分検証やuseMemoの活用も検討。


2-6. 次のステップへ

ここまでで「手動バリデーション」を理解できました。 次の章では、React Hook Form のような専用ライブラリを使って、 より効率的で実用的なフォームバリデーションを実装していきます。

3. React Hook Formによる実践的バリデーション

3-1. ライブラリを使う理由

Reactでフォームを作り込むと、次第に次のような課題が出てきます。

  • 入力値・エラー状態・送信状態など、管理する state が増えて煩雑になる
  • バリデーション処理が重複しやすく、保守が難しくなる
  • 入力ごとに再レンダリングが多発し、パフォーマンスが低下する

こうした問題を解決するのが、軽量フォームライブラリ React Hook Form(RHF) です。 RHFは「Uncontrolled Component」をベースに設計されており、次のような特徴があります。

特徴 説明
高速 入力のたびに全コンポーネントが再レンダリングされない
シンプル register 関数でinput要素を簡単に登録できる
柔軟 独自バリデーションや外部スキーマ(Yupなど)と連携可能
軽量 Formikなどに比べて依存が少なく、動作が軽い

3-2. React Hook Form の基本構文

まず、インストールします。

npm install react-hook-form

続いて、useForm() フックを使ってフォームを構築します。

import React from 'react';
import { useForm } from 'react-hook-form';

function ContactForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = data => {
    console.log('送信データ:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>名前</label>
      <input
        {...register('name', { required: '名前は必須です。' })}
      />
      {errors.name && <p style={{ color: 'red' }}>{errors.name.message}</p>}

      <label>メールアドレス</label>
      <input
        type="email"
        {...register('email', {
          required: 'メールアドレスは必須です。',
          pattern: {
            value: /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
            message: 'メールアドレスの形式が正しくありません。'
          }
        })}
      />
      {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}

      <label>メッセージ</label>
      <textarea
        {...register('message', {
          required: 'メッセージを入力してください。',
          minLength: {
            value: 10,
            message: 'メッセージは10文字以上入力してください。'
          }
        })}
      />
      {errors.message && <p style={{ color: 'red' }}>{errors.message.message}</p>}

      <button type="submit">送信</button>
    </form>
  );
}

export default ContactForm;

ここでのポイントは、register() 関数を使ってフォームフィールドをReact Hook Formに登録している点です。 各項目のバリデーションルールは register の第2引数として定義します。


3-3. バリデーションルールの種類

RHFで設定できる代表的なバリデーションルールは以下の通りです。

ルール 説明
required 入力必須 { required: 'この項目は必須です。' }
minLength / maxLength 文字数制限 { minLength: { value: 6, message: '6文字以上必要です。' } }
pattern 正規表現で形式チェック { pattern: { value: /regex/, message: '形式が不正です。' } }
validate 独自ロジックによるチェック `{ validate: value => value.includes(’@') ‘メール形式が不正です。’ }`

たとえば、独自の条件を追加することも可能です。

<input
  {...register('username', {
    validate: value => {
      if (value.toLowerCase().includes('admin')) {
        return '「admin」は使用できません。';
      }
      return true;
    }
  })}
/>

3-4. Yupと組み合わせたスキーマバリデーション

複数のフォームで同じバリデーションルールを使う場合、 外部ライブラリ Yup を使うとスキーマとしてまとめて管理できます。

npm install @hookform/resolvers yup
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

// スキーマ定義
const schema = yup.object({
  email: yup.string().email('メールアドレスの形式が不正です').required('必須項目です'),
  password: yup.string().min(6, '6文字以上必要です').required('必須項目です')
});

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema)
  });

  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} placeholder="メールアドレス" />
      {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}

      <input type="password" {...register('password')} placeholder="パスワード" />
      {errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>}

      <button type="submit">ログイン</button>
    </form>
  );
}

このようにYupを使うことで、複雑な条件分岐を外部スキーマで宣言的に管理でき、 バリデーションロジックをフォーム本体から分離できます。


3-5. 実践的なUX改善テクニック

React Hook Formでは、以下のようなテクニックでUXを高めることができます。

✅ 入力中にエラーをリセット

ユーザーが修正を始めたら、直前のエラーを即座に消す。

const { register, clearErrors } = useForm();

<input
  {...register('email')}
  onChange={() => clearErrors('email')}
/>

✅ 送信中の状態を管理

formState.isSubmitting でロード中の表示を切り替え。

const { handleSubmit, formState: { isSubmitting } } = useForm();

<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? '送信中…' : '送信'}
</button>

✅ 送信後にフォームをリセット

成功時に reset() を呼び出して入力を初期化。

const { reset } = useForm();
const onSubmit = data => {
  console.log(data);
  reset();
};

3-6. まとめ:React Hook Formを使うべき場面

規模 推奨方法
小規模(2〜3項目) 通常のuseState+手動バリデーションで十分
中規模(5〜10項目) React Hook Formでstate管理とルール定義を簡略化
大規模(複数フォーム再利用) RHF+Yupスキーマで一元化・メンテナンス性向上

次の章では、エラーメッセージの設計とユーザー体験の最適化
(フォームの見せ方・UX設計・アクセシビリティ対応)について解説します。

4. エラーメッセージの設計とUX改善

4-1. エラーメッセージの目的

エラーメッセージは、単に「間違っている」と伝えるためではなく、 「どう直せばいいか」を導くためのインターフェース です。

ユーザーの操作を否定するような表現ではなく、 「次に取るべき行動」が明確に伝わるメッセージが理想です。

良い例:

「パスワードは6文字以上にしてください」

悪い例:

「エラー:入力が正しくありません」


4-2. メッセージデザインの原則

✅ 明確さ(Clarity)

「何が・なぜ・どうすればよいか」を具体的に伝える。

例:

× 入力エラーです
○ メールアドレスの形式が正しくありません(例:user@example.com)

✅ 口調の柔らかさ(Tone)

「エラー」ではなく「確認」「もう一度入力」などの柔らかい表現でストレスを軽減。

✅ 一貫性(Consistency)

複数のフォーム間で表現のルールを統一。 「入力してください」「必須です」などの語尾をそろえることで印象を安定させます。

✅ 即時性(Timeliness)

入力中・フォーカス離れ・送信後など、タイミングに応じて適切に表示/非表示を切り替える。 タイミングが早すぎても遅すぎてもストレスになるため、 「onBlur時+送信時」 の2段階検証が最も自然です。


4-3. UI配置と視覚デザイン

エラーメッセージは 「問題のフィールドのすぐ下」 に配置するのが基本です。 また、ユーザーの視線が自然に流れるようにレイアウトを設計します。

<div className="form-field">
  <label htmlFor="email">メールアドレス</label>
  <input id="email" type="email" {...register('email')} />
  {errors.email && <p className="error-text">{errors.email.message}</p>}
</div>
.form-field {
  margin-bottom: 1.5rem;
}
.error-text {
  color: #ff4d4f;
  font-size: 0.85rem;
  margin-top: 4px;
}

ポイント:

  • エラーは赤系、成功時は緑系で統一する
  • 画面幅が狭い場合はラベルの下に折り返して配置
  • フォーカス時はアウトラインで明示的に視認可能にする

4-4. 成功メッセージと状態表示

成功した場合も「何が成功したか」を明示すると、ユーザーの安心感が高まります。

{isSubmitSuccessful && (
  <p style={{ color: 'green' }}>送信が完了しました</p>
)}

また、送信ボタンを押した後の処理中は「ロード中」状態を明示します。

<button type="submit" disabled={isSubmitting}>
  {isSubmitting ? '送信中...' : '送信'}
</button>

これにより、ユーザーが「クリックしたのに反応がない」と感じるのを防げます。


4-5. アクセシビリティ(A11y)対応

フォームのエラーメッセージは、視覚的だけでなくスクリーンリーダーにも伝わるよう設計します。

aria属性の利用例

<input
  id="email"
  type="email"
  aria-invalid={!!errors.email}
  aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
  <p id="email-error" role="alert">{errors.email.message}</p>
)}

ポイント

  • aria-invalid="true" で入力欄がエラー状態であることを通知
  • aria-describedby でメッセージを紐付ける
  • エラーテキストに role="alert" を指定して即時読み上げ

4-6. 国際化(i18n)と多言語対応

多言語対応を意識する場合、エラーメッセージをハードコードせず、 翻訳ファイルや辞書オブジェクトで管理するのが望ましいです。

const messages = {
  ja: {
    required: 'この項目は必須です。',
    email: 'メールアドレスの形式が正しくありません。'
  },
  en: {
    required: 'This field is required.',
    email: 'Invalid email address format.'
  }
};

これにより、locale の切り替えで即座にフォーム言語を変更できます。 国際向けサービスではUXの差が顕著に出る部分です。


4-7. UX向上の補助要素

要素 内容
✅ プレースホルダー 例:「例:user@example.com」など、入力の期待値を暗示
✅ ヒントテキスト 入力欄下に「半角英数字のみ」などを小さく表示
✅ オートフォーカス 最初の入力欄に自動でフォーカス(ただし乱用注意)
✅ エラースクロール 送信時、最初のエラー項目に自動スクロール
✅ 視覚トランジション エラー表示時にフェードインを使うと自然

これらの補助を組み合わせることで、ユーザーが「修正しやすいフォーム」になります。


4-8. よくあるアンチパターン

アンチパターン 問題点
エラーをまとめて画面上部に出す 視線移動が増え、修正箇所が分かりにくい
エラーメッセージが英語のまま ローカライズ不足でユーザーが理解できない
エラー文をすべて赤く強調 情報量が多すぎて逆に見にくくなる
リアルタイム検証で即エラー 入力途中で「エラー」が出てストレスになる
成功・失敗が視覚的に同じ 状態変化がわからず誤解を招く

4-9. 結果として目指すフォームUX

  • 入力ミスを恐れずに使えるフォーム
  • 修正がしやすく、間違いがすぐ理解できる
  • 「動作している感」があり、安心できる

React Hook Formなどのライブラリと組み合わせれば、 軽量でUXに優れたフォームを簡潔なコードで実現できます。


次章では、バリデーションとフォーム設計を総括し、運用上のベストプラクティス
(コード整理、再利用、スキーマ化、テスト戦略など)について解説します。

5. 実践とベストプラクティス

5-1. 状態管理の分離とカスタムフック化

フォームが複雑になると、useStateuseForm 内で管理する値が増え、 コンポーネントが肥大化しがちです。 その場合は カスタムフック(Custom Hook) にフォームロジックを分離します。

import { useForm } from 'react-hook-form';

export function useLoginForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting }, reset } = useForm();

  const onSubmit = async (data) => {
    console.log('送信:', data);
    await new Promise((resolve) => setTimeout(resolve, 1000)); // 疑似送信
    reset();
  };

  return { register, handleSubmit, errors, isSubmitting, onSubmit };
}

このようにカスタムフック化すると、フォームの表示側は非常にシンプルになります。

import { useLoginForm } from './useLoginForm';

function LoginForm() {
  const { register, handleSubmit, errors, isSubmitting, onSubmit } = useLoginForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: '必須項目です' })} />
      {errors.email && <p>{errors.email.message}</p>}

      <input type="password" {...register('password', { required: '必須項目です' })} />
      {errors.password && <p>{errors.password.message}</p>}

      <button type="submit" disabled={isSubmitting}>送信</button>
    </form>
  );
}

UIとロジックを分離することで、コードの再利用性とテスト性が高まります。


5-2. コンポーネント設計の分割

フォームの入力要素を個別コンポーネント化することで、 UIを統一しつつ保守性を高められます。

function TextField({ label, name, register, error, type = "text" }) {
  return (
    <div className="form-group">
      <label>{label}</label>
      <input type={type} {...register(name)} />
      {error && <p className="error">{error.message}</p>}
    </div>
  );
}
<TextField
  label="メールアドレス"
  name="email"
  type="email"
  register={register('email', { required: 'メールアドレスは必須です' })}
  error={errors.email}
/>

このようにしてフォーム構成要素を統一すれば、大規模アプリでもデザイン崩壊を防げます。


5-3. Yupスキーマの再利用と拡張

バリデーションルールを複数フォームで共有する場合、 Yupスキーマをモジュール化しておくのが有効です。

// schemas/userSchema.js
import * as yup from 'yup';

export const userSchema = yup.object({
  email: yup.string().email('形式が不正です').required('必須項目です'),
  password: yup.string().min(6, '6文字以上必要です').required('必須項目です'),
});
// LoginForm.jsx
import { userSchema } from './schemas/userSchema';
import { yupResolver } from '@hookform/resolvers/yup';
import { useForm } from 'react-hook-form';

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: yupResolver(userSchema),
});

同じスキーマをサインアップやプロフィール更新などにも使い回せるため、 バリデーションの一貫性を維持できます。


5-4. テスト戦略(Jest + Testing Library)

フォームの品質を保つには、ユニットテストやE2Eテストの導入も重要です。

単体テスト例(Jest + React Testing Library)

import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';

test('入力が空の場合エラーが表示される', async () => {
  render(<LoginForm />);
  fireEvent.click(screen.getByText('送信'));
  expect(await screen.findByText('メールアドレスは必須です')).toBeInTheDocument();
});

テスト観点

  • 必須入力・形式チェックが機能するか
  • 成功時の送信イベントが正しく発火するか
  • エラーメッセージが適切に消えるか

フォームの信頼性はUIテストで確認するのが最も確実です。


5-5. 実運用での注意点

項目 内容
CSRF対策 フォーム送信時にトークンを付与してサーバ側で検証
サーババリデーション クライアント検証だけに頼らない
入力制限 maxlength, min, pattern 属性を適切に利用
入力補完 autoComplete="email" などを設定してUXを向上
パフォーマンス useMemo / useCallback を活用して不要な再レンダリングを防止

これらを組み合わせることで、堅牢でユーザーフレンドリーなフォームが実現します。


5-6. 実践的まとめ

  • Reactでは Controlled Component が基本。
  • バリデーションは「送信前」と「onBlur時」の2段階構成が理想。
  • 規模が大きくなる場合は React Hook Form + Yup を採用。
  • エラーメッセージは「行動を導くUI」として設計する。
  • 再利用可能なカスタムフック・スキーマ化・テストを導入することで保守性を確保。

5-7. 次のステップ

次章(#07)では、Reactにおける「状態管理(State Management)」 をテーマに、 useState , useReducer , Context API , そして Zustand / Redux など外部ライブラリを含む 本格的な状態設計の考え方へと進みます。

関連リンク