React

Reactフォーム入門|入力フォームの作成と状態管理

Reactでフォームを扱う際は、入力値をstateで管理する制御コンポーネントのパターンが基本です。テキスト入力、セレクトボックス、チェックボックスなど、さまざまなフォーム要素をReactで管理する方法を解説します。

基本的な使い方

sample.jsx
import React, { useState } from 'react';

function SimpleRegistrationForm() {
  // フォームの状態を管理
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    gender: '',
    agreeToTerms: false,
    role: 'user'
  });

  // エラーメッセージの状態
  const [errors, setErrors] = useState({});
  
  // 送信成功メッセージ
  const [submitSuccess, setSubmitSuccess] = useState(false);

  // 入力フィールドの変更ハンドラー
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    // チェックボックスの場合はchecked値を、それ以外はvalue値を使用
    setFormData(prevData => ({
      ...prevData,
      [name]: type === 'checkbox' ? checked : value
    }));
    
    // 入力時にエラーをクリア
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: null
      }));
    }
  };

  // フォームのバリデーション
  const validateForm = () => {
    const newErrors = {};
    
    // ユーザー名の検証
    if (!formData.username.trim()) {
      newErrors.username = 'ユーザー名は必須です';
    }
    
    // メールアドレスの検証
    if (!formData.email.trim()) {
      newErrors.email = 'メールアドレスは必須です';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = '有効なメールアドレスを入力してください';
    }
    
    // パスワードの検証
    if (!formData.password) {
      newErrors.password = 'パスワードは必須です';
    } else if (formData.password.length < 6) {
      newErrors.password = 'パスワードは6文字以上必要です';
    }
    
    // 利用規約の同意
    if (!formData.agreeToTerms) {
      newErrors.agreeToTerms = '利用規約に同意する必要があります';
    }
    
    return newErrors;
  };

  // フォーム送信ハンドラー
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // バリデーション実行
    const validationErrors = validateForm();
    
    if (Object.keys(validationErrors).length > 0) {
      // エラーがある場合、エラー状態を更新
      setErrors(validationErrors);
    } else {
      // エラーがない場合、フォームを送信
      console.log('送信データ:', formData);
      
      // 実際のアプリではここでAPIにデータを送信
      // この例では成功メッセージを表示
      setSubmitSuccess(true);
      
      // フォームをリセット
      setFormData({
        username: '',
        email: '',
        password: '',
        gender: '',
        agreeToTerms: false,
        role: 'user'
      });
      
      // 3秒後に成功メッセージを非表示
      setTimeout(() => {
        setSubmitSuccess(false);
      }, 3000);
    }
  };

  // スタイル定義
  const styles = {
    formContainer: {
      maxWidth: '500px',
      margin: '0 auto',
      padding: '20px',
      backgroundColor: '#f9f9f9',
      borderRadius: '8px',
      boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
    },
    title: {
      textAlign: 'center',
      color: '#333',
      marginBottom: '20px'
    },
    formGroup: {
      marginBottom: '15px'
    },
    label: {
      display: 'block',
      marginBottom: '5px',
      fontWeight: 'bold'
    },
    input: {
      width: '100%',
      padding: '8px',
      border: '1px solid #ddd',
      borderRadius: '4px',
      fontSize: '16px'
    },
    selectBox: {
      width: '100%',
      padding: '8px',
      border: '1px solid #ddd',
      borderRadius: '4px',
      fontSize: '16px',
      backgroundColor: 'white'
    },
    radioGroup: {
      display: 'flex',
      gap: '15px'
    },
    radioLabel: {
      display: 'flex',
      alignItems: 'center',
      cursor: 'pointer'
    },
    checkboxLabel: {
      display: 'flex',
      alignItems: 'center',
      cursor: 'pointer'
    },
    checkbox: {
      marginRight: '8px'
    },
    error: {
      color: 'red',
      fontSize: '14px',
      marginTop: '5px'
    },
    submitButton: {
      width: '100%',
      padding: '10px',
      backgroundColor: '#4CAF50',
      color: 'white',
      border: 'none',
      borderRadius: '4px',
      fontSize: '16px',
      cursor: 'pointer'
    },
    successMessage: {
      backgroundColor: '#dff0d8',
      color: '#3c763d',
      padding: '10px',
      borderRadius: '4px',
      textAlign: 'center',
      marginBottom: '15px'
    }
  };

  return (
    <div style={styles.formContainer}>
      <h2 style={styles.title}>ユーザー登録</h2>
      
      {/* 送信成功メッセージ */}
      {submitSuccess && (
        <div style={styles.successMessage}>
          登録が完了しました!
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        {/* ユーザー名 */}
        <div style={styles.formGroup}>
          <label style={styles.label} htmlFor="username">ユーザー名</label>
          <input
            style={styles.input}
            type="text"
            id="username"
            name="username"
            value={formData.username}
            onChange={handleChange}
          />
          {errors.username && <p style={styles.error}>{errors.username}</p>}
        </div>
        
        {/* メールアドレス */}
        <div style={styles.formGroup}>
          <label style={styles.label} htmlFor="email">メールアドレス</label>
          <input
            style={styles.input}
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
          />
          {errors.email && <p style={styles.error}>{errors.email}</p>}
        </div>
        
        {/* パスワード */}
        <div style={styles.formGroup}>
          <label style={styles.label} htmlFor="password">パスワード</label>
          <input
            style={styles.input}
            type="password"
            id="password"
            name="password"
            value={formData.password}
            onChange={handleChange}
          />
          {errors.password && <p style={styles.error}>{errors.password}</p>}
        </div>
        
        {/* 性別(ラジオボタン) */}
        <div style={styles.formGroup}>
          <label style={styles.label}>性別</label>
          <div style={styles.radioGroup}>
            <label style={styles.radioLabel}>
              <input
                type="radio"
                name="gender"
                value="male"
                checked={formData.gender === 'male'}
                onChange={handleChange}
              />
              <span style={{ marginLeft: '5px' }}>男性</span>
            </label>
            <label style={styles.radioLabel}>
              <input
                type="radio"
                name="gender"
                value="female"
                checked={formData.gender === 'female'}
                onChange={handleChange}
              />
              <span style={{ marginLeft: '5px' }}>女性</span>
            </label>
            <label style={styles.radioLabel}>
              <input
                type="radio"
                name="gender"
                value="other"
                checked={formData.gender === 'other'}
                onChange={handleChange}
              />
              <span style={{ marginLeft: '5px' }}>その他</span>
            </label>
          </div>
        </div>
        
        {/* 役割(セレクトボックス) */}
        <div style={styles.formGroup}>
          <label style={styles.label} htmlFor="role">役割</label>
          <select
            style={styles.selectBox}
            id="role"
            name="role"
            value={formData.role}
            onChange={handleChange}
          >
            <option value="user">一般ユーザー</option>
            <option value="editor">編集者</option>
            <option value="admin">管理者</option>
          </select>
        </div>
        
        {/* 利用規約(チェックボックス) */}
        <div style={styles.formGroup}>
          <label style={styles.checkboxLabel}>
            <input
              style={styles.checkbox}
              type="checkbox"
              name="agreeToTerms"
              checked={formData.agreeToTerms}
              onChange={handleChange}
            />
            利用規約に同意します
          </label>
          {errors.agreeToTerms && <p style={styles.error}>{errors.agreeToTerms}</p>}
        </div>
        
        {/* 送信ボタン */}
        <button style={styles.submitButton} type="submit">
          登録する
        </button>
      </form>
    </div>
  );
}

export default SimpleRegistrationForm;

説明

Step 1基本的なフォーム処理

Reactでフォームを扱う場合、主に「制御されたコンポーネント(Controlled Components)」と「非制御コンポーネント(Uncontrolled Components)」の2つのアプローチがあります。

制御されたコンポーネントは、Reactの状態(state)によってフォームの入力値を管理するアプローチです:

import React, { useState } from 'react'; function SimpleForm() { const [name, setName] = useState(''); const handleChange = (event) => { setName(event.target.value); }; const handleSubmit = (event) => { event.preventDefault(); alert(`こんにちは、${name}さん!`); }; return ( <form onSubmit={handleSubmit}> <label> 名前: <input type="text" value={name} onChange={handleChange} /> </label> <button type="submit">送信</button> </form> ); }

Step 2複数の入力フィールドの処理

複数の入力フィールドがある場合、それぞれに状態を作成するか、オブジェクトを使用して一括管理できます:

import React, { useState } from 'react'; function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '', message: '' }); const handleChange = (event) => { const { name, value } = event.target; setFormData({ ...formData, // 既存のフォームデータをコピー [name]: value // 変更されたフィールドだけを更新 }); }; const handleSubmit = (event) => { event.preventDefault(); console.log('送信されたデータ:', formData); // ここでAPIにデータを送信するなどの処理 }; return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="name">名前:</label> <input type="text" id="name" name="name" value={formData.name} onChange={handleChange} /> </div> <div> <label htmlFor="email">メールアドレス:</label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} /> </div> <div> <label htmlFor="message">メッセージ:</label> <textarea id="message" name="message" value={formData.message} onChange={handleChange} /> </div> <button type="submit">送信</button> </form> ); }

この例では:

  • フォームデータを一つのオブジェクトとして管理
  • 入力フィールドにはname属性を設定
  • 計算プロパティ名[name]: valueを使用して動的に更新

Step 3フォームバリデーション

ユーザー入力を検証する基本的な方法は、stateに検証エラーを追加することです:

import React, { useState } from 'react'; function SignupForm() { const [formData, setFormData] = useState({ username: '', email: '', password: '', confirmPassword: '' }); const [errors, setErrors] = useState({}); const handleChange = (event) => { const { name, value } = event.target; setFormData({ ...formData, [name]: value }); // フィールド変更時にエラーをクリア if (errors[name]) { setErrors({ ...errors, [name]: null }); } }; const validateForm = () => { const newErrors = {}; // ユーザー名の検証 if (!formData.username.trim()) { newErrors.username = 'ユーザー名は必須です'; } else if (formData.username.length < 3) { newErrors.username = 'ユーザー名は3文字以上必要です'; } // メールアドレスの検証 if (!formData.email.trim()) { newErrors.email = 'メールアドレスは必須です'; } else if (!/\S+@\S+\.\S+/.test(formData.email)) { newErrors.email = '有効なメールアドレスを入力してください'; } // パスワードの検証 if (!formData.password) { newErrors.password = 'パスワードは必須です'; } else if (formData.password.length < 6) { newErrors.password = 'パスワードは6文字以上必要です'; } // パスワード確認 if (formData.password !== formData.confirmPassword) { newErrors.confirmPassword = 'パスワードが一致しません'; } return newErrors; }; const handleSubmit = (event) => { event.preventDefault(); // フォームの検証 const formErrors = validateForm(); if (Object.keys(formErrors).length > 0) { // エラーがある場合 setErrors(formErrors); } else { // エラーがない場合、データを送信 console.log('送信データ:', formData); // ここでAPIにデータを送信する alert('登録が完了しました!'); } }; return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="username">ユーザー名:</label> <input type="text" id="username" name="username" value={formData.username} onChange={handleChange} /> {errors.username && <span className="error">{errors.username}</span>} </div> <div> <label htmlFor="email">メールアドレス:</label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} /> {errors.email && <span className="error">{errors.email}</span>} </div> <div> <label htmlFor="password">パスワード:</label> <input type="password" id="password" name="password" value={formData.password} onChange={handleChange} /> {errors.password && <span className="error">{errors.password}</span>} </div> <div> <label htmlFor="confirmPassword">パスワード(確認):</label> <input type="password" id="confirmPassword" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} /> {errors.confirmPassword && <span className="error">{errors.confirmPassword}</span>} </div> <button type="submit">登録</button> </form> ); }

Step 4他のフォーム要素

さまざまなフォーム要素の扱い方:

  • セレクトボックス(ドロップダウン)
<select name="country" value={formData.country} onChange={handleChange} > <option value="">国を選択</option> <option value="japan">日本</option> <option value="usa">アメリカ</option> <option value="other">その他</option> </select>
  • チェックボックス
const handleCheckboxChange = (event) => { const { name, checked } = event.target; setFormData({ ...formData, [name]: checked }); }; <label> <input type="checkbox" name="agreedToTerms" checked={formData.agreedToTerms} onChange={handleCheckboxChange} /> 利用規約に同意する </label>
  • ラジオボタン
<div> <label>性別:</label> <label> <input type="radio" name="gender" value="male" checked={formData.gender === 'male'} onChange={handleChange} /> 男性 </label> <label> <input type="radio" name="gender" value="female" checked={formData.gender === 'female'} onChange={handleChange} /> 女性 </label> <label> <input type="radio" name="gender" value="other" checked={formData.gender === 'other'} onChange={handleChange} /> その他 </label> </div>
ポイント

ヒント: 大規模なフォームを扱う場合は、以下のライブラリの使用を検討してください:

  • Formik - 制御されたコンポーネントのボイラープレートを減らす
  • React Hook Form - パフォーマンスを重視した非制御コンポーネントベースのライブラリ
  • Yup - フォームバリデーションのためのスキーマビルダー

まとめ

  • ReactのフォームはuseStateで入力値を管理する制御コンポーネントが基本
  • onChangeイベントでstateを更新し、value属性でstateの値を表示する
  • 複数の入力欄は1つのオブジェクトstateでまとめて管理できる
  • onSubmitイベントで送信処理を行い、e.preventDefault()でページリロードを防ぐ
  • バリデーションはリアルタイム(onChange時)と送信時の両方で実装するのが望ましい
  • 大規模なフォームにはReact Hook FormやFormikなどのライブラリの使用を検討する