Reactのstateとは?状態管理の基本概念を10分で理解

Reactの状態管理の基本となるstateの概念を初心者にもわかりやすく解説。useStateの使い方から実践的な活用例まで詳しく紹介します。

Learning Next 運営
51 分で読めます

みなさん、Reactを学び始めて「stateって何?」と思ったことはありませんか?

「HTMLならそのまま値を変更できるのに、なぜこんなに複雑なの?」「stateとpropsの違いが分からない」そんな疑問を持つのは、とても自然なことです。 でも大丈夫です!stateの概念を理解すれば、動的で応答性の高いアプリが作れるようになります。

この記事では、Reactの状態管理の基本から実践的な使い方まで、初心者でも10分で理解できるよう詳しく解説します。 一緒にReactの状態管理をマスターしていきましょう。

stateって何?まずは基本から

stateとは、コンポーネントが持つ「変化する可能性のあるデータ」のことです。

簡単に言うと、ユーザーの操作や時間の経過によって変わる値を管理するための仕組みなんです。

stateの基本的な特徴

stateには、覚えておきたい重要な特徴があります。

  • 動的な値: ユーザーの操作や時間によって変化します
  • コンポーネント固有: 各コンポーネントが独自に管理します
  • 再レンダリングのトリガー: 値が変わると自動的に画面が更新されます
  • 読み取り専用: 直接変更せず、専用の関数で更新します

イメージとしては、コンポーネントの記憶のようなものですね。

stateとpropsの違いを理解しよう

stateとpropsの違いを理解することが、とても重要です。

// propsは親から受け取る「変更できない」データ
function ChildComponent({ name, age }) {
  // name と age は props(変更不可)
  return (
    <div>
      <p>名前: {name}</p>
      <p>年齢: {age}</p>
    </div>
  );
}

// stateは自分で管理する「変更可能な」データ
function ParentComponent() {
  // count は state(変更可能)
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        増加
      </button>
      <ChildComponent name="田中太郎" age={25} />
    </div>
  );
}

違いを表にまとめてみました。

項目stateprops
変更可能性変更可能変更不可
データの出所コンポーネント内親コンポーネント
更新方法setState関数親が再レンダリング
用途動的データ設定・固定データ

この違いを理解すれば、どちらを使うべきかが分かるようになります。

useStateの使い方をマスターしよう

React HooksのuseStateを使って、stateを管理する方法を見てみましょう。

基本的な使い方から、様々なデータタイプまで順番に学んでいきます。

最もシンプルなuseState

まずは、一番基本的なカウンターから始めてみましょう。

import React, { useState } from 'react';

function SimpleCounter() {
  // useState の基本形: [現在の値, 更新関数] = useState(初期値)
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <h2>シンプルカウンター</h2>
      <p>現在の値: {count}</p>
      
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      
      <button onClick={() => setCount(count - 1)}>
        -1
      </button>
      
      <button onClick={() => setCount(0)}>
        リセット
      </button>
    </div>
  );
}

export default SimpleCounter;

このコードを詳しく見てみましょう。

useState(0)で、初期値0のstateを作成しています。 戻り値は配列で、1つ目が現在の値、2つ目が更新用の関数です。

ボタンをクリックすると、setCount関数で値を更新し、画面が自動的に再描画されます。

文字列のstateを扱ってみよう

次は、文字列を扱う例を見てみましょう。

function TextInput() {
  const [message, setMessage] = useState(''); // 初期値は空文字
  
  return (
    <div>
      <h2>メッセージ入力</h2>
      
      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        placeholder="メッセージを入力してください"
      />
      
      <p>入力内容: {message}</p>
      <p>文字数: {message.length}</p>
      
      <button onClick={() => setMessage('')}>
        クリア
      </button>
    </div>
  );
}

onChangeイベントで、入力された値をリアルタイムでstateに反映しています。 e.target.valueで、inputの現在の値を取得できます。

文字数もリアルタイムで表示されるので、ユーザーにとって分かりやすいUIになります。

真偽値(boolean)のstateでON/OFF切り替え

スイッチのような機能を作ってみましょう。

function ToggleSwitch() {
  const [isOn, setIsOn] = useState(false); // 初期値はfalse
  
  return (
    <div>
      <h2>トグルスイッチ</h2>
      
      <p>状態: {isOn ? 'ON' : 'OFF'}</p>
      
      <button onClick={() => setIsOn(!isOn)}>
        {isOn ? 'OFF にする' : 'ON にする'}
      </button>
      
      <div style={{
        width: '50px',
        height: '50px',
        backgroundColor: isOn ? 'green' : 'red',
        borderRadius: '50%'
      }}>
      </div>
    </div>
  );
}

!isOnで現在の値を反転させています。 条件分岐を使って、状態に応じて表示を変えているのがポイントです。

配列のstateでTODOリストを作ろう

もう少し複雑な例として、TODOリストを作ってみましょう。

function TodoList() {
  const [todos, setTodos] = useState([]); // 初期値は空配列
  const [inputValue, setInputValue] = useState('');
  
  const addTodo = () => {
    if (inputValue.trim() !== '') {
      // 新しい配列を作成して追加
      setTodos([...todos, {
        id: Date.now(),
        text: inputValue.trim(),
        completed: false
      }]);
      setInputValue(''); // 入力フィールドをクリア
    }
  };
  
  const toggleTodo = (id) => {
    // mapを使って特定の要素を更新
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };
  
  const deleteTodo = (id) => {
    // filterを使って特定の要素を削除
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <div>
      <h2>TODOリスト</h2>
      
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="新しいTODOを入力"
        />
        <button onClick={addTodo}>追加</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{
              textDecoration: todo.completed ? 'line-through' : 'none'
            }}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
      
      <p>総数: {todos.length}, 完了: {todos.filter(t => t.completed).length}</p>
    </div>
  );
}

このコードには、重要なポイントがいくつかあります。

[...todos, newTodo]で新しい配列を作成しています。 配列の直接変更(pushなど)は避けて、常に新しい配列を作ることが大切です。

mapfilterを使って、配列の要素を更新・削除しています。 これも元の配列を変更せず、新しい配列を作成する方法です。

オブジェクトのstateを上手に扱おう

オブジェクトをstateで管理する場合の、注意点と実装方法を見てみましょう。

複雑なデータを扱う時に、とても役立ちます。

基本的なオブジェクトstate

ユーザープロフィールの管理を例に見てみましょう。

function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
    bio: ''
  });
  
  // オブジェクトの一部を更新する関数
  const updateUser = (field, value) => {
    setUser(prevUser => ({
      ...prevUser,        // 既存のプロパティをコピー
      [field]: value      // 特定のフィールドのみ更新
    }));
  };
  
  return (
    <div>
      <h2>ユーザープロフィール</h2>
      
      <div>
        <label>名前:</label>
        <input
          type="text"
          value={user.name}
          onChange={(e) => updateUser('name', e.target.value)}
        />
      </div>
      
      <div>
        <label>メールアドレス:</label>
        <input
          type="email"
          value={user.email}
          onChange={(e) => updateUser('email', e.target.value)}
        />
      </div>
      
      <div>
        <label>年齢:</label>
        <input
          type="number"
          value={user.age}
          onChange={(e) => updateUser('age', parseInt(e.target.value) || 0)}
        />
      </div>
      
      <div>
        <label>自己紹介:</label>
        <textarea
          value={user.bio}
          onChange={(e) => updateUser('bio', e.target.value)}
          rows="3"
        />
      </div>
      
      <div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f5f5f5' }}>
        <h3>プレビュー</h3>
        <p><strong>名前:</strong> {user.name}</p>
        <p><strong>メール:</strong> {user.email}</p>
        <p><strong>年齢:</strong> {user.age}歳</p>
        <p><strong>自己紹介:</strong> {user.bio}</p>
      </div>
    </div>
  );
}

ここで重要なのは、...prevUserでスプレッド演算子を使っていることです。 これで既存のプロパティをコピーして、特定のフィールドだけを更新できます。

[field]: valueは、動的なプロパティ名を設定する書き方です。 どのフィールドでも同じ関数で更新できるので、コードがスッキリします。

より複雑なオブジェクトstate

ショッピングカートを例に、もう少し複雑なstateを見てみましょう。

function ShoppingCart() {
  const [cart, setCart] = useState({
    items: [],
    total: 0,
    discount: 0,
    shipping: 500
  });
  
  const [products] = useState([
    { id: 1, name: 'リンゴ', price: 100 },
    { id: 2, name: 'バナナ', price: 150 },
    { id: 3, name: 'オレンジ', price: 120 }
  ]);
  
  // 商品をカートに追加
  const addToCart = (product) => {
    setCart(prevCart => {
      const existingItem = prevCart.items.find(item => item.id === product.id);
      
      let newItems;
      if (existingItem) {
        // 既存商品の数量を増加
        newItems = prevCart.items.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        // 新商品を追加
        newItems = [...prevCart.items, { ...product, quantity: 1 }];
      }
      
      // 合計金額を計算
      const newTotal = newItems.reduce((sum, item) => 
        sum + (item.price * item.quantity), 0
      );
      
      return {
        ...prevCart,
        items: newItems,
        total: newTotal
      };
    });
  };
  
  // 商品をカートから削除
  const removeFromCart = (productId) => {
    setCart(prevCart => {
      const newItems = prevCart.items.filter(item => item.id !== productId);
      const newTotal = newItems.reduce((sum, item) => 
        sum + (item.price * item.quantity), 0
      );
      
      return {
        ...prevCart,
        items: newItems,
        total: newTotal
      };
    });
  };
  
  // 割引を適用
  const applyDiscount = (discountPercent) => {
    setCart(prevCart => ({
      ...prevCart,
      discount: discountPercent
    }));
  };
  
  // 最終金額を計算
  const finalTotal = cart.total * (1 - cart.discount / 100) + cart.shipping;
  
  return (
    <div>
      <h2>ショッピングカート</h2>
      
      <div style={{ display: 'flex', gap: '20px' }}>
        <div>
          <h3>商品一覧</h3>
          {products.map(product => (
            <div key={product.id} style={{ marginBottom: '10px' }}>
              <span>{product.name} - ¥{product.price}</span>
              <button 
                onClick={() => addToCart(product)}
                style={{ marginLeft: '10px' }}
              >
                カートに追加
              </button>
            </div>
          ))}
        </div>
        
        <div>
          <h3>カート内容</h3>
          {cart.items.length === 0 ? (
            <p>カートは空です</p>
          ) : (
            <>
              {cart.items.map(item => (
                <div key={item.id} style={{ marginBottom: '10px' }}>
                  <span>{item.name} x {item.quantity} = ¥{item.price * item.quantity}</span>
                  <button 
                    onClick={() => removeFromCart(item.id)}
                    style={{ marginLeft: '10px', color: 'red' }}
                  >
                    削除
                  </button>
                </div>
              ))}
              
              <hr />
              <p>小計: ¥{cart.total}</p>
              <p>配送料: ¥{cart.shipping}</p>
              {cart.discount > 0 && (
                <p>割引 ({cart.discount}%): -¥{Math.round(cart.total * cart.discount / 100)}</p>
              )}
              <p><strong>合計: ¥{Math.round(finalTotal)}</strong></p>
              
              <div>
                <button onClick={() => applyDiscount(10)}>10%割引</button>
                <button onClick={() => applyDiscount(20)}>20%割引</button>
                <button onClick={() => applyDiscount(0)}>割引リセット</button>
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

このコードでは、複数の状態を一つのオブジェクトで管理しています。

商品の追加では、既存商品かどうかを判定して、適切な処理を行っています。 reduceを使って合計金額を計算し、stateを一度に更新しています。

関数型更新(prevCart => ...)を使うことで、確実に最新の状態を取得できます。

stateを使う時の大切なルール

stateを正しく使うための、重要なルールを理解しましょう。

これらのルールを守ることで、バグのない安定したアプリが作れます。

ルール1: stateを直接変更してはいけません

stateの値を直接変更すると、Reactが変更を検知できません。

// ❌ 悪い例(stateを直接変更)
function BadExample() {
  const [items, setItems] = useState([1, 2, 3]);
  
  const addItem = () => {
    items.push(4);        // 直接変更(NG)
    setItems(items);      // 再レンダリングされない
  };
  
  return <button onClick={addItem}>アイテム追加</button>;
}

// ✅ 良い例(新しい配列/オブジェクトを作成)
function GoodExample() {
  const [items, setItems] = useState([1, 2, 3]);
  
  const addItem = () => {
    setItems([...items, 4]); // 新しい配列を作成
  };
  
  return <button onClick={addItem}>アイテム追加</button>;
}

スプレッド演算子(...)を使って、新しい配列やオブジェクトを作成することが重要です。

ルール2: 関数型更新を活用しよう

前の値に基づいて更新する場合は、関数型更新を使いましょう。

function Counter() {
  const [count, setCount] = useState(0);
  
  // ❌ 悪い例(現在の値に依存)
  const badIncrement = () => {
    setCount(count + 1);
    setCount(count + 1); // 同じ値で2回呼ばれる(期待通りにならない)
  };
  
  // ✅ 良い例(関数型更新)
  const goodIncrement = () => {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1); // 正しく2回増加する
  };
  
  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={badIncrement}>悪い増加</button>
      <button onClick={goodIncrement}>良い増加</button>
    </div>
  );
}

関数型更新を使うことで、常に最新の値を取得できます。

ルール3: 複雑なstateは分割しよう

すべてを一つのstateに詰め込むのではなく、関連性に応じて分割しましょう。

// ❌ 悪い例(すべてを一つのstateに詰め込む)
function BadForm() {
  const [formState, setFormState] = useState({
    user: { name: '', email: '' },
    ui: { loading: false, error: null },
    validation: { nameError: '', emailError: '' }
  });
  
  // 更新が複雑になる
  const updateName = (name) => {
    setFormState(prev => ({
      ...prev,
      user: { ...prev.user, name },
      validation: { ...prev.validation, nameError: '' }
    }));
  };
  
  return <div>複雑な更新...</div>;
}

// ✅ 良い例(関連するstateを分割)
function GoodForm() {
  const [user, setUser] = useState({ name: '', email: '' });
  const [loading, setLoading] = useState(false);
  const [errors, setErrors] = useState({});
  
  // シンプルな更新
  const updateName = (name) => {
    setUser(prev => ({ ...prev, name }));
    setErrors(prev => ({ ...prev, name: '' }));
  };
  
  return <div>シンプルな更新...</div>;
}

関連するデータをグループ化して、独立性を保つことが大切です。

実践的なstate活用パターン

実際のアプリケーションでよく使われる、stateのパターンを見てみましょう。

これらのパターンを覚えておくと、様々な場面で応用できます。

検索フィルター機能

商品検索機能を例に、複数のstateを組み合わせた実装を見てみましょう。

function ProductSearch() {
  const [products] = useState([
    { id: 1, name: 'iPhone 14', category: 'スマートフォン', price: 119800 },
    { id: 2, name: 'MacBook Pro', category: 'ノートPC', price: 248800 },
    { id: 3, name: 'iPad Air', category: 'タブレット', price: 84800 },
    { id: 4, name: 'AirPods Pro', category: 'イヤホン', price: 39800 },
    { id: 5, name: 'Apple Watch', category: 'ウェアラブル', price: 59800 }
  ]);
  
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('');
  const [maxPrice, setMaxPrice] = useState(300000);
  const [sortBy, setSortBy] = useState('name');
  
  // フィルタリング処理
  const filteredProducts = products
    .filter(product => 
      product.name.toLowerCase().includes(searchTerm.toLowerCase())
    )
    .filter(product => 
      selectedCategory === '' || product.category === selectedCategory
    )
    .filter(product => 
      product.price <= maxPrice
    )
    .sort((a, b) => {
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      if (sortBy === 'price') return a.price - b.price;
      return 0;
    });
  
  // カテゴリ一覧を取得
  const categories = [...new Set(products.map(product => product.category))];
  
  return (
    <div>
      <h2>商品検索</h2>
      
      <div style={{ marginBottom: '20px', display: 'flex', gap: '15px', flexWrap: 'wrap' }}>
        <input
          type="text"
          placeholder="商品名で検索"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          style={{ padding: '8px' }}
        />
        
        <select
          value={selectedCategory}
          onChange={(e) => setSelectedCategory(e.target.value)}
          style={{ padding: '8px' }}
        >
          <option value="">すべてのカテゴリ</option>
          {categories.map(category => (
            <option key={category} value={category}>{category}</option>
          ))}
        </select>
        
        <div>
          <label>最大価格: ¥{maxPrice.toLocaleString()}</label>
          <input
            type="range"
            min="0"
            max="300000"
            step="10000"
            value={maxPrice}
            onChange={(e) => setMaxPrice(parseInt(e.target.value))}
            style={{ marginLeft: '10px' }}
          />
        </div>
        
        <select
          value={sortBy}
          onChange={(e) => setSortBy(e.target.value)}
          style={{ padding: '8px' }}
        >
          <option value="name">名前順</option>
          <option value="price">価格順</option>
        </select>
      </div>
      
      <p>検索結果: {filteredProducts.length}件</p>
      
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '15px' }}>
        {filteredProducts.map(product => (
          <div key={product.id} style={{ 
            border: '1px solid #ddd', 
            padding: '15px', 
            borderRadius: '8px',
            backgroundColor: '#f9f9f9'
          }}>
            <h3>{product.name}</h3>
            <p>カテゴリ: {product.category}</p>
            <p>価格: ¥{product.price.toLocaleString()}</p>
          </div>
        ))}
      </div>
      
      {filteredProducts.length === 0 && (
        <p style={{ textAlign: 'center', color: '#666', marginTop: '40px' }}>
          条件に合う商品が見つかりませんでした。
        </p>
      )}
    </div>
  );
}

このコードでは、複数のフィルター条件を組み合わせています。

各フィルターが独立したstateで管理されているので、個別に操作できます。 フィルタリング処理は、元の配列に対して順番に適用されています。

タブ切り替え機能

アカウント管理画面のような、タブ切り替え機能を作ってみましょう。

function TabNavigation() {
  const [activeTab, setActiveTab] = useState('profile');
  
  const tabs = [
    { id: 'profile', label: 'プロフィール' },
    { id: 'settings', label: '設定' },
    { id: 'notifications', label: '通知' },
    { id: 'security', label: 'セキュリティ' }
  ];
  
  const renderTabContent = () => {
    switch (activeTab) {
      case 'profile':
        return (
          <div>
            <h3>プロフィール情報</h3>
            <p>名前: 田中太郎</p>
            <p>メール: tanaka@example.com</p>
            <p>登録日: 2024年1月1日</p>
          </div>
        );
      case 'settings':
        return (
          <div>
            <h3>アプリ設定</h3>
            <label>
              <input type="checkbox" /> ダークモード
            </label><br />
            <label>
              <input type="checkbox" /> 自動保存
            </label><br />
            <label>
              言語: 
              <select>
                <option>日本語</option>
                <option>English</option>
              </select>
            </label>
          </div>
        );
      case 'notifications':
        return (
          <div>
            <h3>通知設定</h3>
            <label>
              <input type="checkbox" /> メール通知
            </label><br />
            <label>
              <input type="checkbox" /> プッシュ通知
            </label><br />
            <label>
              <input type="checkbox" /> SMS通知
            </label>
          </div>
        );
      case 'security':
        return (
          <div>
            <h3>セキュリティ設定</h3>
            <button>パスワード変更</button><br /><br />
            <button>二段階認証設定</button><br /><br />
            <button>ログイン履歴を表示</button>
          </div>
        );
      default:
        return <div>コンテンツが見つかりません</div>;
    }
  };
  
  return (
    <div>
      <h2>アカウント管理</h2>
      
      <div style={{ 
        borderBottom: '1px solid #ddd', 
        marginBottom: '20px',
        display: 'flex',
        gap: '0'
      }}>
        {tabs.map(tab => (
          <button
            key={tab.id}
            onClick={() => setActiveTab(tab.id)}
            style={{
              padding: '12px 24px',
              border: 'none',
              backgroundColor: activeTab === tab.id ? '#007bff' : 'transparent',
              color: activeTab === tab.id ? 'white' : '#333',
              borderBottom: activeTab === tab.id ? '3px solid #0056b3' : '3px solid transparent',
              cursor: 'pointer',
              transition: 'all 0.3s ease'
            }}
          >
            {tab.label}
          </button>
        ))}
      </div>
      
      <div style={{ minHeight: '200px', padding: '20px' }}>
        {renderTabContent()}
      </div>
    </div>
  );
}

activeTabのstateでどのタブが選択されているかを管理しています。 タブの配列を使うことで、新しいタブの追加も簡単にできます。

フォームバリデーション

最後に、しっかりとしたバリデーション機能付きのフォームを作ってみましょう。

function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isSubmitted, setIsSubmitted] = useState(false);
  
  // フィールド更新
  const updateField = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    
    // エラーをクリア
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: '' }));
    }
  };
  
  // バリデーション
  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 < 8) {
      newErrors.password = 'パスワードは8文字以上で入力してください';
    }
    
    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'パスワードが一致しません';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  // フォーム送信
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validateForm()) return;
    
    setIsSubmitting(true);
    
    try {
      // 模擬API呼び出し
      await new Promise(resolve => setTimeout(resolve, 2000));
      setIsSubmitted(true);
      
      // フォームリセット
      setFormData({
        username: '',
        email: '',
        password: '',
        confirmPassword: ''
      });
    } catch (error) {
      setErrors({ general: '登録に失敗しました。再度お試しください。' });
    } finally {
      setIsSubmitting(false);
    }
  };
  
  if (isSubmitted) {
    return (
      <div style={{ textAlign: 'center', padding: '40px' }}>
        <h2 style={{ color: 'green' }}>登録完了!</h2>
        <p>アカウントが正常に作成されました。</p>
        <button onClick={() => setIsSubmitted(false)}>
          新しいアカウントを作成
        </button>
      </div>
    );
  }
  
  return (
    <div>
      <h2>アカウント登録</h2>
      
      <form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
        <div style={{ marginBottom: '15px' }}>
          <label>ユーザー名:</label>
          <input
            type="text"
            value={formData.username}
            onChange={(e) => updateField('username', e.target.value)}
            style={{ 
              width: '100%', 
              padding: '8px',
              border: errors.username ? '2px solid red' : '1px solid #ddd'
            }}
          />
          {errors.username && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {errors.username}
            </div>
          )}
        </div>
        
        <div style={{ marginBottom: '15px' }}>
          <label>メールアドレス:</label>
          <input
            type="email"
            value={formData.email}
            onChange={(e) => updateField('email', e.target.value)}
            style={{ 
              width: '100%', 
              padding: '8px',
              border: errors.email ? '2px solid red' : '1px solid #ddd'
            }}
          />
          {errors.email && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {errors.email}
            </div>
          )}
        </div>
        
        <div style={{ marginBottom: '15px' }}>
          <label>パスワード:</label>
          <input
            type="password"
            value={formData.password}
            onChange={(e) => updateField('password', e.target.value)}
            style={{ 
              width: '100%', 
              padding: '8px',
              border: errors.password ? '2px solid red' : '1px solid #ddd'
            }}
          />
          {errors.password && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {errors.password}
            </div>
          )}
        </div>
        
        <div style={{ marginBottom: '15px' }}>
          <label>パスワード確認:</label>
          <input
            type="password"
            value={formData.confirmPassword}
            onChange={(e) => updateField('confirmPassword', e.target.value)}
            style={{ 
              width: '100%', 
              padding: '8px',
              border: errors.confirmPassword ? '2px solid red' : '1px solid #ddd'
            }}
          />
          {errors.confirmPassword && (
            <div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
              {errors.confirmPassword}
            </div>
          )}
        </div>
        
        {errors.general && (
          <div style={{ color: 'red', marginBottom: '15px' }}>
            {errors.general}
          </div>
        )}
        
        <button 
          type="submit" 
          disabled={isSubmitting}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: isSubmitting ? '#ccc' : '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: isSubmitting ? 'not-allowed' : 'pointer'
          }}
        >
          {isSubmitting ? '登録中...' : 'アカウントを作成'}
        </button>
      </form>
    </div>
  );
}

このフォームでは、複数のstateを適切に管理しています。

フォームデータ、エラー、送信状態、完了状態を別々のstateで管理することで、それぞれが独立して動作します。 リアルタイムバリデーションと、フィールド入力時のエラークリアも実装されています。

よくある間違いと対策方法

state使用時によくある間違いと、その対策方法を見てみましょう。

これらを理解することで、バグの少ないコードが書けるようになります。

間違い1: stateの更新タイミングを誤解

stateの更新は非同期なので、すぐには反映されません。

// ❌ 悪い例
function BadExample() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // まだ古い値が表示される
    alert(`現在の値: ${count}`); // 古い値が表示される
  };
  
  return <button onClick={handleClick}>クリック</button>;
}

// ✅ 良い例
function GoodExample() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    const newCount = count + 1;
    setCount(newCount);
    console.log(newCount); // 新しい値が表示される
    alert(`現在の値: ${newCount}`); // 新しい値が表示される
  };
  
  // または useEffect を使用
  React.useEffect(() => {
    console.log('countが更新されました:', count);
  }, [count]);
  
  return <button onClick={handleClick}>クリック</button>;
}

更新後の値をすぐに使いたい場合は、変数に保存してから使いましょう。

間違い2: オブジェクト/配列の直接変更

配列やオブジェクトを直接変更すると、Reactが変更を検知できません。

// ❌ 悪い例
function BadTodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'タスク1', completed: false }
  ]);
  
  const toggleTodo = (id) => {
    const todo = todos.find(t => t.id === id);
    todo.completed = !todo.completed; // 直接変更(NG)
    setTodos(todos); // 再レンダリングされない
  };
  
  return <div>...</div>;
}

// ✅ 良い例
function GoodTodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'タスク1', completed: false }
  ]);
  
  const toggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id 
          ? { ...todo, completed: !todo.completed } // 新しいオブジェクトを作成
          : todo
      )
    );
  };
  
  return <div>...</div>;
}

常に新しい配列やオブジェクトを作成することが重要です。

間違い3: 過度に複雑なstate構造

すべてを一つのstateにまとめると、管理が複雑になります。

// ❌ 悪い例(複雑すぎるstate)
function BadComplexState() {
  const [appState, setAppState] = useState({
    user: {
      profile: { name: '', email: '' },
      preferences: { theme: 'light', language: 'ja' },
      permissions: { canEdit: false, canDelete: false }
    },
    ui: {
      loading: false,
      errors: [],
      modal: { isOpen: false, type: '', data: null }
    },
    data: {
      posts: [],
      comments: [],
      likes: []
    }
  });
  
  // 更新が非常に複雑になる
  const updateUserName = (name) => {
    setAppState(prev => ({
      ...prev,
      user: {
        ...prev.user,
        profile: {
          ...prev.user.profile,
          name
        }
      }
    }));
  };
  
  return <div>...</div>;
}

// ✅ 良い例(適切に分割されたstate)
function GoodSeparateStates() {
  const [userProfile, setUserProfile] = useState({ name: '', email: '' });
  const [userPreferences, setUserPreferences] = useState({ theme: 'light', language: 'ja' });
  const [loading, setLoading] = useState(false);
  const [errors, setErrors] = useState([]);
  const [posts, setPosts] = useState([]);
  
  // シンプルな更新
  const updateUserName = (name) => {
    setUserProfile(prev => ({ ...prev, name }));
  };
  
  return <div>...</div>;
}

関連性の高いデータをグループ化して、適切に分割することが大切です。

まとめ

Reactのstateについて、基本概念から実践的な活用方法まで詳しく解説しました。

stateの重要ポイント

state管理で押さえておきたいポイントをまとめます。

  • 動的データの管理: 変化する可能性のあるデータを適切に扱う
  • 再レンダリングのトリガー: state変更時に自動的に画面が更新される
  • イミュータブル更新: 直接変更せず、常に新しい値を作成する
  • 適切な粒度: 関連するデータをまとめ、独立性を保つ

効果的なstate活用のコツ

実際の開発で役立つコツをご紹介します。

  1. 段階的な学習: 基本的な値から複雑なオブジェクトまで徐々に覚える
  2. 実践的な練習: 実際のアプリケーション機能で手を動かして学ぶ
  3. パターンの理解: よくある使用パターンを身につける
  4. 間違いから学習: よくある失敗例を理解して回避する

次のステップ

stateの基本を理解したら、以下の概念も学習してみてください。

  • useEffect: 副作用の処理とstateとの組み合わせ
  • useContext: コンポーネント間でのstate共有
  • useReducer: より複雑なstate管理
  • カスタムフック: stateロジックの再利用

Reactのstateは、動的なWebアプリケーションを作る上で欠かせない重要な概念です。

基本をしっかりと理解して、実践を重ねることで、より高度なReactアプリケーションが開発できるようになります。 まずは簡単な例から始めて、徐々に複雑な機能にチャレンジしてみてくださいね。

関連記事