React useStateの実務的な使い方|実装パターンとベストプラクティス

React / Next.js

React useStateの実務的な使い方|実装パターンとベストプラクティス

はじめに:useStateの基本概念

React の useState は、関数コンポーネントで状態を管理するための最も基本的なフックです。しかし、実務プロジェクトでは単なる値の保持以上の工夫が必要になります。本記事では、教科書的な例ではなく、実際のプロダクション環境で活用される実装パターンを解説します。

useState の基本的な使い方:

  • 状態値と更新関数をペアで取得
  • 再レンダリングをトリガー
  • 不変性を保つ更新が重要

業務でよく遭遇する実務ユースケース

1. フォーム入力管理の複雑化

実務では、単一の入力フィールドではなく、複数フィールドを持つフォームを扱うことがほとんどです。ユーザー登録フォーム、検索フィルター、設定画面など、複数の状態を一度に管理する必要があります。

2. API通信の状態管理

データ取得時のローディング状態、エラーハンドリング、成功時のデータ保持などを同時に管理する必要があります。

3. UI状態とビジネスロジックの分離

モーダルの開閉、タブの選択状態、ページネーション など UI に関わる状態と、ユーザー情報やショッピングカート内容などのビジネスロジック的な状態を分けて管理することが重要です。

4. 非同期処理と状態更新のタイミング

複数の非同期処理が走る環境で、状態の更新順序やレース条件への対応が必要になります。

実装コード:実務的なパターン

パターン1:複数フィールドを持つフォーム管理

典型的なユースケースとして、ユーザープロフィール更新フォームを例に挙げます。

// UserProfileForm.tsx
import React, { useState } from 'react';

interface UserProfile {
  name: string;
  email: string;
  age: number;
  bio: string;
  isPublic: boolean;
}

interface FormErrors {
  name?: string;
  email?: string;
  age?: string;
}

export const UserProfileForm: React.FC = () => {
  // フォーム入力状態
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: 0,
    bio: '',
    isPublic: false,
  });

  // バリデーションエラー状態
  const [errors, setErrors] = useState({});

  // 送信処理の状態
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitSuccess, setSubmitSuccess] = useState(false);
  const [submitError, setSubmitError] = useState(null);

  // 変更検知状態(未保存警告用)
  const [isDirty, setIsDirty] = useState(false);

  const handleInputChange = (
    e: React.ChangeEvent
  ) => {
    const { name, value, type } = e.currentTarget;
    
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' 
        ? (e.currentTarget as HTMLInputElement).checked 
        : value,
    }));

    // 入力があれば未保存状態に
    setIsDirty(true);
    
    // 入力値に対するリアルタイムバリデーション
    validateField(name, value);
  };

  const validateField = (name: string, value: string) => {
    let newError: string | undefined;

    switch (name) {
      case 'name':
        if (value.trim().length === 0) {
          newError = '名前は必須です';
        } else if (value.length > 50) {
          newError = '名前は50文字以内で入力してください';
        }
        break;
      case 'email':
        const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
        if (!emailRegex.test(value)) {
          newError = '有効なメールアドレスを入力してください';
        }
        break;
      case 'age':
        const ageNum = parseInt(value, 10);
        if (isNaN(ageNum) || ageNum < 0 || ageNum > 150) {
          newError = '年齢は0〜150の数値で入力してください';
        }
        break;
    }

    setErrors(prev => ({
      ...prev,
      [name]: newError,
    }));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // バリデーション確認
    if (Object.keys(errors).length > 0) {
      setSubmitError('入力内容をご確認ください');
      return;
    }

    setIsSubmitting(true);
    setSubmitError(null);
    setSubmitSuccess(false);

    try {
      const response = await fetch('/api/user/profile', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });

      if (!response.ok) {
        throw new Error('プロフィール更新に失敗しました');
      }

      setSubmitSuccess(true);
      setIsDirty(false);
      
      // 成功メッセージは3秒後に消える
      setTimeout(() => setSubmitSuccess(false), 3000);
    } catch (error) {
      setSubmitError(
        error instanceof Error ? error.message : 'エラーが発生しました'
      );
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    
{errors.name && {errors.name}}
{errors.email && {errors.email}}
{errors.age && {errors.age}}