React inputの実装|フォーム入力の基本から応用まで

Reactでのinput要素の実装方法を基本から応用まで詳しく解説。制御コンポーネント、バリデーション、カスタムインプットの作成方法を実践的に紹介

Learning Next 運営
55 分で読めます

みなさん、Reactでフォームを作る時に困ったことはありませんか?

「inputの値が思うように更新されない」「バリデーションの実装がうまくいかない」そんな悩みを感じたことがある方も多いはずです。

実は、Reactでのフォーム実装には少しコツがあるんです。 この記事では、Reactでのinput要素の実装方法を基本から応用まで、分かりやすくお伝えします。 制御コンポーネントの作り方からカスタムインプットまで、実践的な内容をご紹介しますので、ぜひ参考にしてくださいね。

Reactでinputを扱う前に知っておきたいこと

Reactでinput要素を扱う際の基本的な考え方を理解しましょう。

従来のHTMLとは少し違うアプローチが必要なんです。

制御コンポーネントと非制御コンポーネントの違い

Reactでは、input要素を扱う方法が2つあります。

まずは両方のパターンを見てみましょう。

// ❌ 非制御コンポーネント(推奨されない)
function UncontrolledInput() {
  const inputRef = useRef();

  const handleSubmit = () => {
    console.log(inputRef.current.value); // DOMから直接値を取得
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleSubmit}>送信</button>
    </div>
  );
}

このコードでは、useRefを使ってDOMから直接値を取得しています。 でも、この方法はReactらしくないんです。

// ✅ 制御コンポーネント(推奨)
function ControlledInput() {
  const [value, setValue] = useState('');

  const handleChange = (e) => {
    setValue(e.target.value); // Reactの状態で値を管理
  };

  const handleSubmit = () => {
    console.log(value); // 状態から値を取得
  };

  return (
    <div>
      <input 
        type="text" 
        value={value} 
        onChange={handleChange} 
      />
      <button onClick={handleSubmit}>送信</button>
    </div>
  );
}

こちらの方法では、ReactのuseStateで値を管理しています。 制御コンポーネントと呼ばれるこの方法が、Reactでは推奨されています。

なぜなら、Reactの状態管理と一体化することで、より予測可能で管理しやすいコードになるからです。

基本的なinput要素の実装

それでは、様々なタイプのinput要素を実装してみましょう。

まずは全体像をご覧ください。

function BasicInputForm() {
  const [formData, setFormData] = useState({
    text: '',
    email: '',
    password: '',
    number: 0,
    date: '',
    checkbox: false,
    radio: '',
    textarea: '',
    select: ''
  });

  // 汎用的な変更ハンドラ
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form Data:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 各種input要素 */}
    </form>
  );
}

ちょっと長いですが、大丈夫です! 一つずつ見ていきますね。

まず、状態管理の部分から。

const [formData, setFormData] = useState({
  text: '',
  email: '',
  password: '',
  // 他のフィールド...
});

フォームの全ての値をオブジェクトで管理しています。 この方法だと、フィールドが増えても管理しやすいんです。

次に、変更ハンドラを見てみましょう。

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

この関数が、すべてのinput要素で使えるんです。 typeによってチェックボックスかどうかを判断しています。

テキスト入力の実装はこんな感じです。

<div>
  <label htmlFor="text">テキスト:</label>
  <input
    id="text"
    name="text"
    type="text"
    value={formData.text}
    onChange={handleChange}
    placeholder="テキストを入力"
  />
</div>

valueで現在の値を設定し、onChangeで変更を検知します。 name属性が重要で、これが状態のキーと対応しています。

チェックボックスの場合は少し違います。

<div>
  <label>
    <input
      name="checkbox"
      type="checkbox"
      checked={formData.checkbox}
      onChange={handleChange}
    />
    利用規約に同意する
  </label>
</div>

valueではなくcheckedを使うのがポイントです。

input状態管理のベストプラクティス

効率的なinput状態管理にはいくつかのパターンがあります。

どの方法を選ぶかで、コードの見通しが大きく変わるんです。

パターン1: 個別の状態管理

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

  return (
    <form>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        placeholder="名前"
      />
      <input 
        value={email} 
        onChange={(e) => setEmail(e.target.value)} 
        placeholder="メール"
      />
      <input 
        value={age} 
        onChange={(e) => setAge(e.target.value)} 
        placeholder="年齢"
      />
    </form>
  );
}

フィールドが少ない場合はシンプルで分かりやすいです。 でも、フィールドが増えると管理が大変になります。

パターン2: オブジェクトでの一括管理

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

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

  return (
    <form>
      <input 
        value={formData.name} 
        onChange={handleChange('name')} 
        placeholder="名前"
      />
      <input 
        value={formData.email} 
        onChange={handleChange('email')} 
        placeholder="メール"
      />
      <input 
        value={formData.age} 
        onChange={handleChange('age')} 
        placeholder="年齢"
      />
    </form>
  );
}

中規模のフォームに適しています。 関数をカリー化することで、各フィールド専用のハンドラを作っています。

パターン3: useReducerを使った管理

function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        [action.field]: action.value
      };
    case 'RESET_FORM':
      return action.initialState;
    default:
      return state;
  }
}

まず、reducerを定義します。 UPDATE_FIELDでフィールドを更新し、RESET_FORMでリセットできます。

function ReducerStateForm() {
  const initialState = { name: '', email: '', age: '' };
  const [formData, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (field) => (e) => {
    dispatch({
      type: 'UPDATE_FIELD',
      field,
      value: e.target.value
    });
  };

  const handleReset = () => {
    dispatch({
      type: 'RESET_FORM',
      initialState
    });
  };

  return (
    <form>
      <input 
        value={formData.name} 
        onChange={handleChange('name')} 
        placeholder="名前"
      />
      <input 
        value={formData.email} 
        onChange={handleChange('email')} 
        placeholder="メール"
      />
      <input 
        value={formData.age} 
        onChange={handleChange('age')} 
        placeholder="年齢"
      />
      <button type="button" onClick={handleReset}>
        リセット
      </button>
    </form>
  );
}

複雑な状態変更が必要な場合は、useReducerが便利です。 アクションを定義することで、状態変更のパターンが明確になります。

バリデーション機能の実装

フォームには必ずバリデーションが必要ですよね。

ユーザーの入力をチェックして、適切にフィードバックする機能を作ってみましょう。

リアルタイムバリデーションの基本

まずは、入力と同時にチェックするバリデーションです。

全体の構造はこんな感じになります。

function ValidatedForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // バリデーション関数
  const validateField = (name, value) => {
    // 各フィールドのチェック処理
  };

  // その他のハンドラ
}

フォームデータに加えて、errorstouchedも管理しています。 touchedは「ユーザーがそのフィールドに触れたか」を記録するためです。

バリデーション関数を詳しく見てみましょう。

const validateField = (name, value) => {
  switch (name) {
    case 'name':
      if (!value.trim()) return '名前は必須です';
      if (value.length < 2) return '名前は2文字以上で入力してください';
      return '';

    case 'email':
      if (!value.trim()) return 'メールアドレスは必須です';
      const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
      if (!emailRegex.test(value)) return '正しいメールアドレスを入力してください';
      return '';

    case 'password':
      if (!value) return 'パスワードは必須です';
      if (value.length < 8) return 'パスワードは8文字以上で入力してください';
      if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
        return 'パスワードは大文字、小文字、数字を含む必要があります';
      }
      return '';

    case 'confirmPassword':
      if (!value) return 'パスワード確認は必須です';
      if (value !== formData.password) return 'パスワードが一致しません';
      return '';

    default:
      return '';
  }
};

各フィールドごとに異なるチェックを行っています。 エラーがなければ空文字を返すのがポイントです。

入力の変更ハンドラはこうなります。

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

  // リアルタイムバリデーション
  if (touched[name]) {
    const error = validateField(name, value);
    setErrors(prev => ({
      ...prev,
      [name]: error
    }));
  }
};

touched[name]trueの場合のみバリデーションを実行します。 これで、ユーザーが一度フィールドを触った後からチェックが始まります。

blur時(フィールドからフォーカスが外れた時)の処理も重要です。

const handleBlur = (e) => {
  const { name, value } = e.target;
  
  setTouched(prev => ({
    ...prev,
    [name]: true
  }));

  const error = validateField(name, value);
  setErrors(prev => ({
    ...prev,
    [name]: error
  }));
};

フィールドを離れた時にtouchedtrueにして、バリデーションを実行します。

カスタムバリデーションフックの作成

繰り返し使えるバリデーション機能を作ってみましょう。

まずは、フックの全体像をご覧ください。

function useFormValidation(initialValues, validationRules) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // バリデーション関数
  const validateField = useCallback((name, value) => {
    const rules = validationRules[name];
    if (!rules) return '';

    // 各種チェック処理
  }, [validationRules, values]);

  // その他の関数
}

このフックを使えば、どんなフォームでも簡単にバリデーションが追加できます。

validateField関数の中身を見てみましょう。

const validateField = useCallback((name, value) => {
  const rules = validationRules[name];
  if (!rules) return '';

  // required チェック
  if (rules.required && (!value || value.toString().trim() === '')) {
    return rules.required;
  }

  // 値が空の場合、required以外のバリデーションはスキップ
  if (!value || value.toString().trim() === '') {
    return '';
  }

  // minLength チェック
  if (rules.minLength && value.length < rules.minLength.value) {
    return rules.minLength.message;
  }

  // maxLength チェック
  if (rules.maxLength && value.length > rules.maxLength.value) {
    return rules.maxLength.message;
  }

  // pattern チェック
  if (rules.pattern && !rules.pattern.value.test(value)) {
    return rules.pattern.message;
  }

  // custom バリデーション
  if (rules.custom) {
    return rules.custom(value, values);
  }

  return '';
}, [validationRules, values]);

ルールオブジェクトに基づいて、段階的にチェックしています。 customルールでは、独自のバリデーション関数を実行できます。

使用例はこんな感じです。

function RegistrationForm() {
  const validationRules = {
    username: {
      required: 'ユーザー名は必須です',
      minLength: {
        value: 3,
        message: 'ユーザー名は3文字以上で入力してください'
      },
      pattern: {
        value: /^[a-zA-Z0-9_]+$/,
        message: 'ユーザー名は英数字とアンダースコアのみ使用可能です'
      }
    },
    email: {
      required: 'メールアドレスは必須です',
      pattern: {
        value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
        message: '正しいメールアドレスを入力してください'
      }
    },
    password: {
      required: 'パスワードは必須です',
      minLength: {
        value: 8,
        message: 'パスワードは8文字以上で入力してください'
      },
      pattern: {
        value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
        message: 'パスワードは大文字、小文字、数字を含む必要があります'
      }
    },
    confirmPassword: {
      required: 'パスワード確認は必須です',
      custom: (value, values) => {
        if (value !== values.password) {
          return 'パスワードが一致しません';
        }
        return '';
      }
    }
  };

  const {
    values,
    setValue,
    setTouched,
    validateAllFields,
    reset,
    getFieldError,
    isValid
  } = useFormValidation(
    { username: '', email: '', password: '', confirmPassword: '' },
    validationRules
  );

  // フォームの実装
}

ルールを定義するだけで、複雑なバリデーションが簡単に実装できます。

非同期バリデーション

サーバーでのチェックが必要な場合もありますよね。

ユーザー名の重複チェックなどを実装してみましょう。

function AsyncValidationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: ''
  });
  const [errors, setErrors] = useState({});
  const [validating, setValidating] = useState({});

  // ユーザー名の重複チェック
  const validateUsername = useCallback(
    debounce(async (username) => {
      if (!username || username.length < 3) return;

      setValidating(prev => ({ ...prev, username: true }));

      try {
        const response = await fetch(`/api/check-username/${username}`);
        const data = await response.json();
        
        setErrors(prev => ({
          ...prev,
          username: data.available ? '' : 'このユーザー名は既に使用されています'
        }));
      } catch (error) {
        setErrors(prev => ({
          ...prev,
          username: 'ユーザー名の確認に失敗しました'
        }));
      } finally {
        setValidating(prev => ({ ...prev, username: false }));
      }
    }, 500),
    []
  );

  // 入力変更時の処理
  const handleChange = (e) => {
    const { name, value } = e.target;
    
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));

    // リアルタイム非同期バリデーション
    if (name === 'username') {
      validateUsername(value);
    }
  };

  return (
    <form>
      <div className="form-group">
        <label htmlFor="username">ユーザー名</label>
        <div className="input-container">
          <input
            id="username"
            name="username"
            type="text"
            value={formData.username}
            onChange={handleChange}
            placeholder="ユーザー名を入力"
          />
          {validating.username && (
            <span className="validating-indicator">確認中...</span>
          )}
        </div>
        {errors.username && (
          <span className="error-message">{errors.username}</span>
        )}
      </div>
    </form>
  );
}

debounce関数で入力の間隔を制御しています。 これで、ユーザーが入力を止めてから0.5秒後にチェックが実行されます。

デバウンス関数はこんな感じです。

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

連続した入力に対して、最後の入力から指定時間経過後に関数を実行します。

カスタムインプットコンポーネントの作成

再利用可能なinputコンポーネントを作ってみましょう。

一度作っておけば、プロジェクト全体で使い回せて便利です。

汎用的なInputコンポーネント

まずは、基本的なInputコンポーネントの全体像をご覧ください。

const Input = forwardRef(({
  label,
  error,
  type = 'text',
  required = false,
  placeholder,
  helperText,
  startIcon,
  endIcon,
  className = '',
  ...props
}, ref) => {
  const [isFocused, setIsFocused] = useState(false);
  const [showPassword, setShowPassword] = useState(false);

  const inputId = useId();
  const isPassword = type === 'password';

  // イベントハンドラやその他の処理

  return (
    <div className={`input-wrapper ${className}`}>
      {/* 実際のJSX */}
    </div>
  );
});

機能豊富なコンポーネントですが、使う側はシンプルに扱えます。

パスワード表示切り替えの処理を見てみましょう。

const handleFocus = (e) => {
  setIsFocused(true);
  props.onFocus?.(e);
};

const handleBlur = (e) => {
  setIsFocused(false);
  props.onBlur?.(e);
};

const togglePasswordVisibility = () => {
  setShowPassword(!showPassword);
};

const inputType = isPassword && showPassword ? 'text' : type;

フォーカスの状態を管理して、パスワードの表示/非表示を切り替えています。

実際のJSX部分はこうなります。

return (
  <div className={`input-wrapper ${className}`}>
    {label && (
      <label htmlFor={inputId} className="input-label">
        {label}
        {required && <span className="required-marker">*</span>}
      </label>
    )}
    
    <div className={`input-container ${isFocused ? 'focused' : ''} ${error ? 'error' : ''}`}>
      {startIcon && (
        <span className="input-icon start-icon">{startIcon}</span>
      )}
      
      <input
        ref={ref}
        id={inputId}
        type={inputType}
        placeholder={placeholder}
        className="input-field"
        onFocus={handleFocus}
        onBlur={handleBlur}
        {...props}
      />
      
      {isPassword && (
        <button
          type="button"
          className="input-icon end-icon password-toggle"
          onClick={togglePasswordVisibility}
          tabIndex={-1}
        >
          {showPassword ? '🙈' : '👁️'}
        </button>
      )}
      
      {endIcon && !isPassword && (
        <span className="input-icon end-icon">{endIcon}</span>
      )}
    </div>
    
    {error && (
      <span className="error-message">{error}</span>
    )}
    
    {helperText && !error && (
      <span className="helper-text">{helperText}</span>
    )}
  </div>
);

ラベル、アイコン、エラーメッセージ、ヘルプテキストなど、必要な要素を条件的に表示しています。

使用例はこんな感じです。

function CustomInputDemo() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    search: ''
  });

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

  return (
    <div className="form-demo">
      <h2>カスタムインプットデモ</h2>
      
      <Input
        label="ユーザー名"
        value={formData.username}
        onChange={handleChange('username')}
        placeholder="ユーザー名を入力"
        required
        helperText="3文字以上で入力してください"
        startIcon="👤"
      />
      
      <Input
        label="パスワード"
        type="password"
        value={formData.password}
        onChange={handleChange('password')}
        placeholder="パスワードを入力"
        required
        helperText="8文字以上で入力してください"
      />
    </div>
  );
}

シンプルな記述で、機能豊富なinputが使えます。

数値専用のInputコンポーネント

数値入力専用のコンポーネントも作ってみましょう。

数値の入力にはいくつかの特殊な要件があります。

const NumberInput = forwardRef(({
  label,
  value,
  onChange,
  min,
  max,
  step = 1,
  precision = 0,
  prefix,
  suffix,
  allowNegative = true,
  placeholder,
  error,
  ...props
}, ref) => {
  const [displayValue, setDisplayValue] = useState('');
  const [isFocused, setIsFocused] = useState(false);

  // 数値のフォーマット
  const formatNumber = (num) => {
    if (num === null || num === undefined || num === '') return '';
    
    const number = parseFloat(num);
    if (isNaN(number)) return '';
    
    return precision > 0 ? number.toFixed(precision) : number.toString();
  };

  // 表示値の更新
  useEffect(() => {
    setDisplayValue(formatNumber(value));
  }, [value, precision]);

  // その他の処理
});

表示用の値と実際の値を分けて管理しているのがポイントです。

入力の変更処理を見てみましょう。

const handleInputChange = (e) => {
  let inputValue = e.target.value;
  
  // 負の値が許可されていない場合は除去
  if (!allowNegative) {
    inputValue = inputValue.replace('-', '');
  }
  
  // 数値以外の文字を除去(小数点、負号は保持)
  inputValue = inputValue.replace(/[^0-9.-]/g, '');
  
  // 複数の小数点を防ぐ
  const decimalCount = (inputValue.match(/\./g) || []).length;
  if (decimalCount > 1) {
    inputValue = inputValue.substring(0, inputValue.lastIndexOf('.'));
  }
  
  setDisplayValue(inputValue);
  
  // 数値に変換して onChange を呼び出し
  const numericValue = parseFloat(inputValue);
  if (!isNaN(numericValue)) {
    // min/max の制限をチェック
    let clampedValue = numericValue;
    if (min !== undefined) clampedValue = Math.max(min, clampedValue);
    if (max !== undefined) clampedValue = Math.min(max, clampedValue);
    
    onChange?.(clampedValue);
  } else if (inputValue === '' || inputValue === '-') {
    onChange?.(null);
  }
};

入力値を正規化して、範囲チェックも行っています。

増減ボタンの実装はこうなります。

const increment = () => {
  const newValue = (value || 0) + step;
  const clampedValue = max !== undefined ? Math.min(max, newValue) : newValue;
  onChange?.(clampedValue);
};

const decrement = () => {
  const newValue = (value || 0) - step;
  const clampedValue = min !== undefined ? Math.max(min, newValue) : newValue;
  onChange?.(clampedValue);
};

数値の増減も範囲内に収まるようにしています。

自動サイズ調整テキストエリア

テキストエリアも便利な機能を追加してみましょう。

内容に応じて高さが自動調整されるテキストエリアです。

const AutoResizeTextarea = forwardRef(({
  label,
  value = '',
  onChange,
  placeholder,
  error,
  minRows = 3,
  maxRows = 10,
  showCharCount = false,
  maxLength,
  helperText,
  required = false,
  ...props
}, ref) => {
  const textareaRef = useRef();
  const [charCount, setCharCount] = useState(0);

  // refの統合
  const combinedRef = useMemo(() => {
    return (node) => {
      textareaRef.current = node;
      if (typeof ref === 'function') {
        ref(node);
      } else if (ref) {
        ref.current = node;
      }
    };
  }, [ref]);

  // 高さの自動調整
  const adjustHeight = useCallback(() => {
    const textarea = textareaRef.current;
    if (!textarea) return;

    // 一時的にheightをautoにして自然な高さを取得
    textarea.style.height = 'auto';
    
    // 行の高さを計算
    const lineHeight = parseInt(getComputedStyle(textarea).lineHeight);
    const minHeight = lineHeight * minRows;
    const maxHeight = lineHeight * maxRows;
    
    // スクロール高さを取得
    const scrollHeight = textarea.scrollHeight;
    
    // 制限内で高さを設定
    const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
    textarea.style.height = `${newHeight}px`;
  }, [minRows, maxRows]);

  // 値の変更時に高さを調整
  useEffect(() => {
    adjustHeight();
    setCharCount(value.length);
  }, [value, adjustHeight]);

  // その他の処理
});

自動サイズ調整の仕組みは、一度height: autoにしてからscrollHeightを取得し、最小・最大行数の範囲内で高さを設定しています。

Markdownエディタの機能も追加してみましょう。

function MarkdownEditor({ label, value, onChange, ...props }) {
  const [isPreviewMode, setIsPreviewMode] = useState(false);
  const [previewContent, setPreviewContent] = useState('');

  // Markdownのプレビュー生成(簡易版)
  const generatePreview = (markdown) => {
    return markdown
      .replace(/^### (.*$)/gim, '<h3>$1</h3>')
      .replace(/^## (.*$)/gim, '<h2>$1</h2>')
      .replace(/^# (.*$)/gim, '<h1>$1</h1>')
      .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
      .replace(/\*(.*)\*/gim, '<em>$1</em>')
      .replace(/
/gim, '<br>');
  };

  useEffect(() => {
    if (isPreviewMode) {
      setPreviewContent(generatePreview(value || ''));
    }
  }, [value, isPreviewMode]);

  const togglePreview = () => {
    setIsPreviewMode(!isPreviewMode);
  };

  return (
    <div className="markdown-editor">
      <div className="markdown-editor-header">
        <span>{label}</span>
        <button
          type="button"
          onClick={togglePreview}
          className="preview-toggle"
        >
          {isPreviewMode ? '編集' : 'プレビュー'}
        </button>
      </div>
      
      {isPreviewMode ? (
        <div 
          className="markdown-preview"
          dangerouslySetInnerHTML={{ __html: previewContent }}
        />
      ) : (
        <AutoResizeTextarea
          value={value}
          onChange={onChange}
          placeholder="Markdownで入力してください..."
          showCharCount
          maxLength={5000}
          minRows={5}
          maxRows={20}
          helperText="**太字** *斜体* # 見出し1 ## 見出し2"
          {...props}
        />
      )}
    </div>
  );
}

編集モードとプレビューモードを切り替えられるMarkdownエディタです。 実際のプロジェクトでは、より高機能なMarkdownパーサーを使うことをおすすめします。

フォームライブラリとの統合

Reactのフォームライブラリとカスタムコンポーネントを組み合わせてみましょう。

より効率的で保守性の高いフォームが作れます。

React Hook Formとの統合

まずは、React Hook Formと組み合わせる方法です。

Controllerコンポーネントを使って、カスタムコンポーネントを統合します。

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

// React Hook Formで使用可能なInput wrapper
const FormInput = ({ name, control, rules, ...props }) => {
  return (
    <Controller
      name={name}
      control={control}
      rules={rules}
      render={({ field, fieldState: { error } }) => (
        <Input
          {...field}
          {...props}
          error={error?.message}
        />
      )}
    />
  );
};

Controllerで包むことで、カスタムコンポーネントでもReact Hook Formの機能が使えます。

数値入力コンポーネントの場合は少し注意が必要です。

const FormNumberInput = ({ name, control, rules, ...props }) => {
  return (
    <Controller
      name={name}
      control={control}
      rules={rules}
      render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
        <NumberInput
          {...field}
          {...props}
          value={value}
          onChange={onChange}
          error={error?.message}
        />
      )}
    />
  );
};

onChangevalueを明示的に受け取って、NumberInputに渡しています。

実際の使用例はこうなります。

function ReactHookFormExample() {
  const {
    control,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
    watch
  } = useForm({
    defaultValues: {
      name: '',
      email: '',
      age: null,
      bio: '',
      terms: false
    }
  });

  const watchAge = watch('age');

  const onSubmit = async (data) => {
    console.log('Form Data:', data);
    
    // 模擬的な非同期処理
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    alert('フォームが送信されました!');
    reset();
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <h2>React Hook Form統合例</h2>
      
      <FormInput
        name="name"
        control={control}
        label="名前"
        placeholder="お名前を入力"
        rules={{
          required: '名前は必須です',
          minLength: {
            value: 2,
            message: '名前は2文字以上で入力してください'
          }
        }}
        required
      />
      
      <FormNumberInput
        name="age"
        control={control}
        label="年齢"
        min={0}
        max={120}
        suffix="歳"
        rules={{
          required: '年齢は必須です',
          min: {
            value: 18,
            message: '18歳以上である必要があります'
          }
        }}
        required
      />
      
      {watchAge && watchAge < 18 && (
        <div className="warning-message">
          ⚠️ 18歳未満の方は保護者の同意が必要です
        </div>
      )}
      
      <div className="form-actions">
        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? '送信中...' : '送信'}
        </button>
        <button type="button" onClick={() => reset()}>
          リセット
        </button>
      </div>
    </form>
  );
}

React Hook Formの機能(バリデーション、状態管理、送信処理など)がすべて使えます。 watchで特定のフィールドの値を監視することもできて便利です。

Formikとの統合

Formikライブラリとの組み合わせも見てみましょう。

YupとFormikを組み合わせたバリデーションが特徴的です。

import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

// Formik用のInput wrapper
const FormikInput = ({ label, ...props }) => (
  <Field>
    {({ field, meta }) => (
      <Input
        {...field}
        {...props}
        label={label}
        error={meta.touched && meta.error ? meta.error : ''}
      />
    )}
  </Field>
);

// バリデーションスキーマ
const validationSchema = Yup.object({
  name: Yup.string()
    .min(2, '名前は2文字以上で入力してください')
    .required('名前は必須です'),
  email: Yup.string()
    .email('正しいメールアドレスを入力してください')
    .required('メールアドレスは必須です'),
  age: Yup.number()
    .positive('年齢は正の数で入力してください')
    .integer('年齢は整数で入力してください')
    .min(18, '18歳以上である必要があります')
    .max(120, '120歳以下で入力してください')
    .required('年齢は必須です'),
});

Yupのスキーマは宣言的で読みやすいのが特徴です。

実際のフォーム実装はこうなります。

function FormikExample() {
  const handleSubmit = async (values, { setSubmitting, resetForm }) => {
    console.log('Form Data:', values);
    
    // 模擬的な非同期処理
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    alert('フォームが送信されました!');
    resetForm();
    setSubmitting(false);
  };

  return (
    <Formik
      initialValues={{
        name: '',
        email: '',
        age: '',
      }}
      validationSchema={validationSchema}
      onSubmit={handleSubmit}
    >
      {({ isSubmitting, values, setFieldValue }) => (
        <Form>
          <h2>Formik統合例</h2>
          
          <FormikInput
            name="name"
            label="名前"
            placeholder="お名前を入力"
            required
          />
          
          <FormikInput
            name="email"
            type="email"
            label="メールアドレス"
            placeholder="example@mail.com"
            required
          />
          
          <Field name="age">
            {({ field, meta }) => (
              <NumberInput
                label="年齢"
                value={field.value}
                onChange={(value) => setFieldValue('age', value)}
                onBlur={() => setFieldTouched('age', true)}
                min={0}
                max={120}
                suffix="歳"
                error={meta.touched && meta.error ? meta.error : ''}
                required
              />
            )}
          </Field>
          
          <div className="form-actions">
            <button type="submit" disabled={isSubmitting}>
              {isSubmitting ? '送信中...' : '送信'}
            </button>
          </div>
        </Form>
      )}
    </Formik>
  );
}

FormikもReact Hook Formも、それぞれに良さがあります。 プロジェクトの要件に応じて選択してくださいね。

まとめ

Reactでのinput実装について、基本から応用まで見てきました。

最後に、重要なポイントをまとめておきますね。

覚えておきたい重要なポイント

input実装で特に大切なことをまとめてみました。

制御コンポーネントの活用 Reactの状態でinputの値を管理することで、予測可能で管理しやすいコードになります。 useStateuseReducerを使って、フォームの状態を適切に管理しましょう。

適切なバリデーション リアルタイムバリデーションと非同期バリデーションを組み合わせることで、ユーザーフレンドリーなフォームを作れます。 touched状態を使って、適切なタイミングでエラーを表示するのがコツです。

再利用可能なコンポーネント カスタムinputコンポーネントを作ることで、開発効率が大幅に向上します。 アクセシビリティやユーザビリティも考慮して設計しましょう。

ライブラリとの組み合わせ React Hook FormやFormikなどのライブラリと組み合わせることで、より効率的な開発が可能になります。 プロジェクトの規模や要件に応じて選択してください。

プロジェクトに応じたパターン選択

どの実装パターンを選ぶべきか、目安をお伝えしますね。

// 小規模なフォーム(3-5フィールド)
// - useState での直接管理
// - 基本的なバリデーション
// - シンプルな実装で十分

// 中規模なフォーム(6-15フィールド)
// - カスタムフックでの状態管理
// - 汎用的なバリデーション
// - 再利用可能なコンポーネント

// 大規模なフォーム(16フィールド以上)
// - React Hook Form / Formik の活用
// - 複雑なバリデーションルール
// - カスタムコンポーネントライブラリ

// エンタープライズレベル
// - デザインシステムとの統合
// - 国際化対応
// - アクセシビリティ完全対応

規模に応じて適切な手法を選ぶことが大切です。

これからの学習ステップ

input実装をさらに極めるための学習の進め方をご提案します。

1. 基本の定着 制御コンポーネントの理解を深めて、useStateuseReducerでの状態管理をマスターしましょう。 基本的なバリデーションの実装パターンも覚えておくと役立ちます。

2. 実践的な機能の習得 カスタムフックの作成方法を学んで、非同期バリデーションの実装にもチャレンジしてみてください。 複雑なinputコンポーネントの作成も経験値になります。

3. ライブラリの活用 React Hook FormやFormikの使い方を覚えて、バリデーションライブラリとの組み合わせも試してみましょう。 UIコンポーネントライブラリとの統合も実践的です。

4. 高度なテクニック パフォーマンス最適化やアクセシビリティ対応、テスト手法まで学べば、プロレベルの実装ができるようになります。

Reactでのinput実装は、フロントエンド開発の基礎となる重要なスキルです。

制御コンポーネントの概念をしっかりと理解して、実践的なバリデーションやカスタムコンポーネントの作成方法を身につけることで、ユーザーにとって使いやすいフォームが作れるようになります。

今回紹介した実装例を参考にして、ぜひあなたのプロジェクトで活用してみてください。 継続的に学習することで、どんどん上達していけるはずです。

関連記事