AI時代のReact入門 第9回:状態管理②(useStateの実践)

「複数の入力フォームの状態をどう管理すればいい?」 「状態更新のベストプラクティスって?」

useStateの基本は理解できたけれど、実際のアプリケーション開発ではもっと複雑な状態管理が必要になりますよね。今回は、useStateを使った実践的な状態管理のテクニックを紹介します。

前回学んだ基礎知識を活かしながら、フォーム管理やパフォーマンス最適化など、現場で使える実践的なテクニックを解説していきます。この記事を読めば、より複雑な状態管理も自信を持って実装できるようになるはずです。

useStateの実践:現場で使える状態管理テクニック

「複数の入力フォームの状態をどう管理すればいい?」 「状態更新のベストプラクティスって?」

useStateの基本は理解できたけれど、実際のアプリケーション開発ではもっと複雑な状態管理が必要になりますよね。今回は、useStateを使った実践的な状態管理のテクニックを紹介します。

前回学んだ基礎知識を活かしながら、フォーム管理やパフォーマンス最適化など、現場で使える実践的なテクニックを解説していきます。この記事を読めば、より複雑な状態管理も自信を持って実装できるようになるはずです。

複数の状態管理パターン

実際のアプリケーションでは、複数の状態を同時に管理する必要があります。主なパターンを見ていきましょう。

1. 個別のuseStateで管理

jsx
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');

このパターンは:

  • シンプルで分かりやすい
  • 状態の更新が独立している
  • コードが少し冗長になる可能性がある

2. オブジェクトでまとめて管理

jsx
const [formData, setFormData] = useState({
  firstName: '',
  lastName: '',
  email: ''
});

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

このパターンは:

  • まとまった数の状態を扱いやすい
  • 一括で状態を管理できる
  • オブジェクトの更新に注意が必要

実践的な実装例

1. マルチステップフォーム

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

const MultiStepForm = () => {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    personalInfo: {
      firstName: '',
      lastName: '',
      email: ''
    },
    address: {
      postalCode: '',
      prefecture: '',
      city: ''
    },
    confirmation: false
  });

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

  const handleSubmit = (e) => {
    e.preventDefault();
    if (step < 3) {
      setStep(prev => prev + 1);
    } else {
      alert('送信しました!');
    }
  };

  const renderStep = () => {
    switch (step) {
      case 1:
        return (
          <div className="space-y-4">
            <h2 className="text-xl font-bold">個人情報</h2>
            <div>
              <label className="block text-sm font-medium">姓</label>
              <input
                type="text"
                value={formData.personalInfo.lastName}
                onChange={(e) => handleChange('personalInfo', 'lastName', e.target.value)}
                className="mt-1 block w-full rounded border-gray-300 shadow-sm"
              />
            </div>
            <div>
              <label className="block text-sm font-medium">名</label>
              <input
                type="text"
                value={formData.personalInfo.firstName}
                onChange={(e) => handleChange('personalInfo', 'firstName', e.target.value)}
                className="mt-1 block w-full rounded border-gray-300 shadow-sm"
              />
            </div>
            <div>
              <label className="block text-sm font-medium">メール</label>
              <input
                type="email"
                value={formData.personalInfo.email}
                onChange={(e) => handleChange('personalInfo', 'email', e.target.value)}
                className="mt-1 block w-full rounded border-gray-300 shadow-sm"
              />
            </div>
          </div>
        );
      case 2:
        return (
          <div className="space-y-4">
            <h2 className="text-xl font-bold">住所情報</h2>
            <div>
              <label className="block text-sm font-medium">郵便番号</label>
              <input
                type="text"
                value={formData.address.postalCode}
                onChange={(e) => handleChange('address', 'postalCode', e.target.value)}
                className="mt-1 block w-full rounded border-gray-300 shadow-sm"
              />
            </div>
            <div>
              <label className="block text-sm font-medium">都道府県</label>
              <input
                type="text"
                value={formData.address.prefecture}
                onChange={(e) => handleChange('address', 'prefecture', e.target.value)}
                className="mt-1 block w-full rounded border-gray-300 shadow-sm"
              />
            </div>
            <div>
              <label className="block text-sm font-medium">市区町村</label>
              <input
                type="text"
                value={formData.address.city}
                onChange={(e) => handleChange('address', 'city', e.target.value)}
                className="mt-1 block w-full rounded border-gray-300 shadow-sm"
              />
            </div>
          </div>
        );
      case 3:
        return (
          <div className="space-y-4">
            <h2 className="text-xl font-bold">確認</h2>
            <div className="p-4 bg-gray-50 rounded">
              <h3 className="font-medium">個人情報</h3>
              <p>氏名: {formData.personalInfo.lastName} {formData.personalInfo.firstName}</p>
              <p>メール: {formData.personalInfo.email}</p>
              
              <h3 className="font-medium mt-4">住所情報</h3>
              <p>郵便番号: {formData.address.postalCode}</p>
              <p>都道府県: {formData.address.prefecture}</p>
              <p>市区町村: {formData.address.city}</p>
            </div>
            <div>
              <label className="flex items-center">
                <input
                  type="checkbox"
                  checked={formData.confirmation}
                  onChange={(e) => setFormData(prev => ({ ...prev, confirmation: e.target.checked }))}
                  className="rounded"
                />
                <span className="ml-2">入力内容に間違いありません</span>
              </label>
            </div>
          </div>
        );
      default:
        return null;
    }
  };

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
      <div className="mb-6">
        <div className="flex justify-between items-center">
          {[1, 2, 3].map((num) => (
            <div
              key={num}
              className={`w-8 h-8 rounded-full flex items-center justify-center ${
                num === step ? 'bg-blue-500 text-white' : 'bg-gray-200'
              }`}
            >
              {num}
            </div>
          ))}
        </div>
      </div>
      
      <form onSubmit={handleSubmit}>
        {renderStep()}
        
        <div className="mt-6 flex justify-between">
          {step > 1 && (
            <button
              type="button"
              onClick={() => setStep(prev => prev - 1)}
              className="px-4 py-2 text-gray-600 border rounded"
            >
              戻る
            </button>
          )}
          <button
            type="submit"
            disabled={step === 3 && !formData.confirmation}
            className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
          >
            {step === 3 ? '送信' : '次へ'}
          </button>
        </div>
      </form>
    </div>
  );
};

export default MultiStepForm;

2. 買い物カートの実装

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

const ShoppingCart = () => {
  const [items, setItems] = useState([
    { id: 1, name: 'Tシャツ', price: 2000, quantity: 0 },
    { id: 2, name: 'ジーンズ', price: 5000, quantity: 0 },
    { id: 3, name: '帽子', price: 1500, quantity: 0 }
  ]);

  const updateQuantity = (id, delta) => {
    setItems(prev => prev.map(item => {
      if (item.id === id) {
        const newQuantity = Math.max(0, item.quantity + delta);
        return { ...item, quantity: newQuantity };
      }
      return item;
    }));
  };

  const calculateTotal = () => {
    return items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  };

  return (
    <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
      <h2 className="text-xl font-bold mb-4">ショッピングカート</h2>
      
      <div className="space-y-4">
        {items.map(item => (
          <div key={item.id} className="flex items-center justify-between p-4 border rounded">
            <div>
              <h3 className="font-medium">{item.name}</h3>
              <p className="text-gray-600">¥{item.price.toLocaleString()}</p>
            </div>
            
            <div className="flex items-center space-x-2">
              <button
                onClick={() => updateQuantity(item.id, -1)}
                className="w-8 h-8 bg-gray-100 rounded-full"
              >
                -
              </button>
              <span className="w-8 text-center">{item.quantity}</span>
              <button
                onClick={() => updateQuantity(item.id, 1)}
                className="w-8 h-8 bg-gray-100 rounded-full"
              >
                +
              </button>
            </div>
          </div>
        ))}
      </div>
      
      <div className="mt-6 p-4 bg-gray-50 rounded">
        <div className="flex justify-between items-center">
          <span className="font-medium">合計</span>
          <span className="text-xl">¥{calculateTotal().toLocaleString()}</span>
        </div>
      </div>
      
      <button
        className="mt-4 w-full py-2 bg-blue-500 text-white rounded disabled:opacity-50"
        disabled={calculateTotal() === 0}
      >
        購入する
      </button>
    </div>
  );
};

export default ShoppingCart;

パフォーマンス最適化のポイント

1. 状態の分割

jsx
// ❌ 大きな状態を1つのオブジェクトで管理
const [state, setState] = useState({
  userData: {},
  cart: [],
  ui: { isModalOpen: false, theme: 'light' }
});

// ✅ 関連する状態ごとに分割
const [userData, setUserData] = useState({});
const [cartItems, setCartItems] = useState([]);
const [uiState, setUiState] = useState({
  isModalOpen: false,
  theme: 'light'
});

2. コールバック関数の最適化

jsx
// ❌ 毎回新しい関数が生成される
const handleClick = () => {
  setCount(count + 1);
};

// ✅ 関数の参照が安定する
const handleClick = useCallback(() => {
  setCount(prev => prev + 1);
}, []);

よくある間違いと解決策

1. 状態更新の非同期性を考慮していない

jsx
// ❌ 状態更新の直後に値を使用
setCount(count + 1);
console.log(count);  // 更新前の値が表示される

// ✅ useEffectで状態の変更を監視
useEffect(() => {
  console.log(count);  // 更新後の値が表示される
}, [count]);

2. 配列やオブジェクトの更新を間違える

jsx
// ❌ 直接変更
const [items, setItems] = useState([1, 2, 3]);
items.push(4);  // NG

// ✅ 新しい配列を作成
setItems([...items, 4]);

まとめ

実践的なuseStateの使い方について学びました:

  • 複数の状態管理には、個別のuseStateとオブジェクトでの管理の2つのアプローチがある
  • 複雑なフォームは適切に状態を分割して管理する
  • パフォーマンス最適化には状態の分割と適切なメモ化が重要
  • 状態更新は必ず新しいオブジェクトや配列を作成する

次回は、「イベントハンドリング」について学んでいきます。今回学んだ状態管理と組み合わせて、より洗練されたインタラクションを実装する方法を解説していきます。