ReactのuseStateが分からない|初心者向け完全ガイド

React useStateの基本から実践的な使い方まで初心者向けに詳しく解説。状態管理の概念、更新方法、よくあるエラーと解決方法を実践的なサンプルコードとともに紹介します。

Learning Next 運営
50 分で読めます

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

「なぜ普通の変数だと画面が更新されないの?」 「useStateの使い方がよく分からない」 「状態管理って難しそう...」

こんな風に思った方も多いでしょう。

実は、useStateはReact開発において最も重要な機能の一つなんです。 この記事では、useStateの基本から実践的な使い方まで、初心者の方にも分かりやすく解説していきますよ。

状態管理の概念から、よくあるエラーの解決方法まで、実際のコード例と一緒に学んでいきましょう。

useStateって何?基本を理解しよう

まず、「そもそもuseStateって何?」という疑問から解決していきましょう。

状態管理の基本的な仕組み

useStateは、コンポーネントが値を覚えておくための仕組みです。

簡単に言うと、コンポーネントの「記憶力」を与えてくれる機能ですね。

// 普通の変数だと、こんな問題が起こります
const BadExample = () => {
  let count = 0; // 普通の変数

  const handleClick = () => {
    count = count + 1; // 値は変わるが...
    console.log(count); // コンソールには表示される
    // でも画面は更新されない!
  };

  return (
    <div>
      <p>カウント: {count}</p> {/* 常に0のまま */}
      <button onClick={handleClick}>増やす</button>
    </div>
  );
};

この例を見てみましょう。

let count = 0で変数を宣言しています。 ボタンをクリックするとcountの値は確かに増えます。

でも、画面には反映されません。 なぜでしょうか?

Reactでは、普通の変数が変わっただけでは画面を更新してくれないからなんです。

次に、useStateを使った例を見てみましょう。

import React, { useState } from 'react';

const GoodExample = () => {
  const [count, setCount] = useState(0); // useStateを使用

  const handleClick = () => {
    setCount(count + 1); // setCountで値を更新
    // 画面も自動的に更新される!
  };

  return (
    <div>
      <p>カウント: {count}</p> {/* 値がちゃんと表示される */}
      <button onClick={handleClick}>増やす</button>
    </div>
  );
};

今度はuseState(0)を使っています。

この書き方のポイントを説明しますね。

const [count, setCount] = useState(0)の部分です。 ここで2つのものを受け取っています。

  • count: 現在の値
  • setCount: 値を更新するための関数

setCount(count + 1)で値を更新すると、Reactが「あ、値が変わったな」と気づいて、自動的に画面を更新してくれます。

これがuseStateの基本的な仕組みです。

useStateの基本的な書き方

useStateの書き方にはパターンがあります。

// 基本的な文法
const [状態の値, 更新関数] = useState(初期値);

// 具体的な例をいくつか見てみましょう
const [count, setCount] = useState(0);        // 数値
const [name, setName] = useState('');         // 文字列
const [isVisible, setIsVisible] = useState(false); // 真偽値
const [items, setItems] = useState([]);       // 配列
const [user, setUser] = useState({});         // オブジェクト

この書き方は「分割代入」と呼ばれるJavaScriptの機能です。

useStateは配列を返してくれます。 その配列の1番目が「現在の値」、2番目が「更新関数」です。

// useStateの中身を詳しく見ると...
const stateArray = useState(0); // [0, function]
const currentValue = stateArray[0]; // 現在の値
const updateFunction = stateArray[1]; // 更新関数

// これを短縮したのが通常の書き方
const [value, setValue] = useState(0);

関数の名前は「set + 変数名」にするのが一般的です。

例えば、countならsetCountnameならsetNameという感じですね。

数値を扱ってみよう

それでは、実際にuseStateを使って数値を扱ってみましょう。

シンプルなカウンター

まずは基本的なカウンターから始めます。

const SimpleCounter = () => {
  const [count, setCount] = useState(0);

  // 値を増やす
  const increment = () => {
    setCount(count + 1);
  };

  // 値を減らす
  const decrement = () => {
    setCount(count - 1);
  };

  // 値をリセット
  const reset = () => {
    setCount(0);
  };

  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h2>シンプルカウンター</h2>
      <p style={{ fontSize: '24px', fontWeight: 'bold' }}>
        現在の値: {count}
      </p>
      <div>
        <button onClick={decrement}>-1</button>
        <button onClick={reset} style={{ margin: '0 10px' }}>
          リセット
        </button>
        <button onClick={increment}>+1</button>
      </div>
    </div>
  );
};

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

まず、状態の宣言部分です。

const [count, setCount] = useState(0);

初期値を0に設定しています。

次に、値を変更する関数を作っています。

const increment = () => {
  setCount(count + 1);
};

setCountに新しい値を渡すことで、状態を更新しています。 count + 1で現在の値に1を足した値を設定しますね。

同じように、減らす関数とリセット関数も作っています。

const decrement = () => {
  setCount(count - 1);
};

const reset = () => {
  setCount(0);
};

これらの関数をボタンのonClickに設定すると、クリックで値が変わります。

より安全な更新方法

実は、状態を更新するときにより安全な方法があります。

const SafeCounter = () => {
  const [count, setCount] = useState(0);

  // 前の値を使った更新(推奨方法)
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h2>安全なカウンター</h2>
      <p style={{ fontSize: '24px' }}>{count}</p>
      <button onClick={decrement}>-1</button>
      <button onClick={increment}>+1</button>
    </div>
  );
};

ここで注目したいのは、更新の仕方です。

setCount(prevCount => prevCount + 1);

この書き方では、関数をsetCountに渡しています。 この関数は前の値(prevCount)を受け取って、新しい値を返します。

なぜこの方法が良いのでしょうか?

実は、Reactの状態更新は非同期で処理されることがあります。 そのため、直接countを参照すると、古い値を使ってしまう可能性があるんです。

関数形式で書くと、常に最新の値を使って更新できるので安全ですね。

文字列を扱ってみよう

次は、文字列の状態管理を見てみましょう。

入力フォームの作成

文字列の状態管理でよく使われるのが、入力フォームです。

const TextInput = () => {
  const [name, setName] = useState('');
  const [message, setMessage] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name && message) {
      alert(`${name}さんからのメッセージ: ${message}`);
      // フォームをリセット
      setName('');
      setMessage('');
    }
  };

  return (
    <div style={{ padding: '20px', maxWidth: '500px' }}>
      <h2>メッセージフォーム</h2>
      
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '15px' }}>
          <label>
            名前:
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="お名前を入力"
              style={{ 
                width: '100%', 
                padding: '8px', 
                marginTop: '5px'
              }}
            />
          </label>
        </div>
        
        <div style={{ marginBottom: '15px' }}>
          <label>
            メッセージ:
            <textarea
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              placeholder="メッセージを入力"
              rows={4}
              style={{ 
                width: '100%', 
                padding: '8px', 
                marginTop: '5px'
              }}
            />
          </label>
        </div>
        
        <button 
          type="submit"
          disabled={!name || !message}
          style={{
            padding: '10px 20px',
            backgroundColor: (!name || !message) ? '#ccc' : '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px'
          }}
        >
          送信
        </button>
      </form>
      
      <div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f8f9fa' }}>
        <h3>プレビュー</h3>
        <p><strong>名前:</strong> {name || '(未入力)'}</p>
        <p><strong>メッセージ:</strong> {message || '(未入力)'}</p>
        <p><strong>文字数:</strong> {message.length} 文字</p>
      </div>
    </div>
  );
};

このコードの重要な部分を見てみましょう。

まず、2つの状態を定義しています。

const [name, setName] = useState('');
const [message, setMessage] = useState('');

どちらも文字列なので、初期値は空文字('')に設定しています。

次に、入力フィールドとの連携部分です。

<input
  type="text"
  value={name}
  onChange={(e) => setName(e.target.value)}
/>

value={name}で現在の状態を表示します。 onChangeで入力値が変わったときに状態を更新しますね。

e.target.valueは、入力フィールドに入力された値を取得します。

テキストエリアも同じような仕組みです。

<textarea
  value={message}
  onChange={(e) => setMessage(e.target.value)}
/>

フォーム送信の処理も見てみましょう。

const handleSubmit = (e) => {
  e.preventDefault();
  if (name && message) {
    alert(`${name}さんからのメッセージ: ${message}`);
    // フォームをリセット
    setName('');
    setMessage('');
  }
};

e.preventDefault()でページのリロードを防いでいます。 入力チェックをして、問題なければアラートを表示します。

最後に状態をリセットして、フォームを空にしています。

真偽値を扱ってみよう

boolean値(true/false)の状態管理も見てみましょう。

表示切り替えの実装

真偽値は、何かの表示/非表示を切り替えるときによく使います。

const ToggleExample = () => {
  const [isVisible, setIsVisible] = useState(false);
  const [isDarkMode, setIsDarkMode] = useState(false);
  const [isLoading, setIsLoading] = useState(false);

  const toggleVisibility = () => {
    setIsVisible(!isVisible);
  };

  const toggleDarkMode = () => {
    setIsDarkMode(!isDarkMode);
  };

  const simulateLoading = () => {
    setIsLoading(true);
    
    // 3秒後にローディングを終了
    setTimeout(() => {
      setIsLoading(false);
    }, 3000);
  };

  const containerStyle = {
    padding: '20px',
    backgroundColor: isDarkMode ? '#333' : 'white',
    color: isDarkMode ? 'white' : 'black',
    transition: 'all 0.3s ease'
  };

  return (
    <div style={containerStyle}>
      <h2>表示切り替えの例</h2>
      
      <div style={{ marginBottom: '20px' }}>
        <button onClick={toggleVisibility}>
          {isVisible ? '隠す' : '表示する'}
        </button>
        
        {isVisible && (
          <div style={{ 
            marginTop: '10px', 
            padding: '15px', 
            backgroundColor: isDarkMode ? '#555' : '#f0f0f0'
          }}>
            <p>🎉 この内容が表示されました!</p>
            <p>ボタンで表示/非表示を切り替えられます。</p>
          </div>
        )}
      </div>
      
      <div style={{ marginBottom: '20px' }}>
        <label>
          <input
            type="checkbox"
            checked={isDarkMode}
            onChange={(e) => setIsDarkMode(e.target.checked)}
            style={{ marginRight: '8px' }}
          />
          ダークモード
        </label>
      </div>
      
      <div>
        <button 
          onClick={simulateLoading}
          disabled={isLoading}
          style={{
            padding: '10px 20px',
            backgroundColor: isLoading ? '#ccc' : '#28a745',
            color: 'white',
            border: 'none'
          }}
        >
          {isLoading ? 'ローディング中...' : 'データを読み込む'}
        </button>
        
        {isLoading && (
          <div style={{ marginTop: '10px' }}>
            <p>データを読み込んでいます...</p>
          </div>
        )}
      </div>
    </div>
  );
};

このコードの面白い部分を見てみましょう。

まず、3つの真偽値を管理しています。

const [isVisible, setIsVisible] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
const [isLoading, setIsLoading] = useState(false);

それぞれ異なる目的で使っています。

表示切り替えの部分です。

const toggleVisibility = () => {
  setIsVisible(!isVisible);
};

!isVisibleで現在の値を反転させています。 trueならfalseに、falseならtrueになりますね。

条件付きレンダリングも重要なポイントです。

{isVisible && (
  <div>
    <p>🎉 この内容が表示されました!</p>
  </div>
)}

isVisibleがtrueのときだけ、divが表示されます。 これを「条件付きレンダリング」と呼びます。

チェックボックスとの連携も見てみましょう。

<input
  type="checkbox"
  checked={isDarkMode}
  onChange={(e) => setIsDarkMode(e.target.checked)}
/>

checked={isDarkMode}で現在の状態をチェックボックスに反映します。 e.target.checkedでチェックボックスの状態を取得しますね。

配列を扱ってみよう

配列の状態管理は少し複雑ですが、とても便利です。

Todoリストの作成

配列を使った典型的な例として、Todoリストを作ってみましょう。

const TodoList = () => {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [nextId, setNextId] = useState(1);

  // Todoを追加
  const addTodo = () => {
    if (inputValue.trim()) {
      const newTodo = {
        id: nextId,
        text: inputValue,
        completed: false,
        createdAt: new Date().toLocaleString()
      };
      
      setTodos([...todos, newTodo]); // 新しい配列を作成
      setInputValue('');
      setNextId(nextId + 1);
    }
  };

  // Todoを削除
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // Todoの完了状態を切り替え
  const toggleTodo = (id) => {
    setTodos(todos.map(todo => 
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };

  return (
    <div style={{ padding: '20px', maxWidth: '600px' }}>
      <h2>Todoリスト</h2>
      
      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="新しいタスクを入力"
          style={{
            padding: '10px',
            width: '300px',
            border: '1px solid #ddd'
          }}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
        />
        <button 
          onClick={addTodo}
          style={{
            padding: '10px 20px',
            marginLeft: '10px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none'
          }}
        >
          追加
        </button>
      </div>
      
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(todo => (
          <li 
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              padding: '12px',
              marginBottom: '8px',
              backgroundColor: todo.completed ? '#f8f9fa' : 'white',
              border: '1px solid #dee2e6'
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
              style={{ marginRight: '12px' }}
            />
            
            <div style={{ flex: 1 }}>
              <span style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
              }}>
                {todo.text}
              </span>
              <div style={{ fontSize: '12px', color: '#6c757d' }}>
                作成日時: {todo.createdAt}
              </div>
            </div>
            
            <button
              onClick={() => deleteTodo(todo.id)}
              style={{
                padding: '6px 12px',
                backgroundColor: '#dc3545',
                color: 'white',
                border: 'none'
              }}
            >
              削除
            </button>
          </li>
        ))}
      </ul>
      
      {todos.length === 0 && (
        <div style={{ 
          textAlign: 'center', 
          padding: '40px', 
          color: '#6c757d'
        }}>
          <p>📝 まだタスクがありません</p>
          <p>上のフォームから新しいタスクを追加してみましょう</p>
        </div>
      )}
    </div>
  );
};

このコードの重要なポイントを解説しますね。

まず、配列の状態を定義しています。

const [todos, setTodos] = useState([]);

初期値は空の配列([])です。

Todo追加の処理を詳しく見てみましょう。

const addTodo = () => {
  if (inputValue.trim()) {
    const newTodo = {
      id: nextId,
      text: inputValue,
      completed: false,
      createdAt: new Date().toLocaleString()
    };
    
    setTodos([...todos, newTodo]); // スプレッド演算子で新しい配列作成
  }
};

ここで重要なのはsetTodos([...todos, newTodo])の部分です。

...todosはスプレッド演算子と呼ばれます。 既存の配列をコピーして、最後に新しい要素を追加しています。

元の配列を直接変更してはいけません。 必ず新しい配列を作成する必要があります。

削除処理も見てみましょう。

const deleteTodo = (id) => {
  setTodos(todos.filter(todo => todo.id !== id));
};

filterメソッドで、指定されたID以外の要素だけを残した新しい配列を作成しています。

更新処理はもう少し複雑です。

const toggleTodo = (id) => {
  setTodos(todos.map(todo => 
    todo.id === id 
      ? { ...todo, completed: !todo.completed }
      : todo
  ));
};

mapメソッドで各要素をチェックしています。 指定されたIDの要素だけcompletedを反転させて、他はそのまま返しています。

配列のレンダリングも重要なポイントです。

{todos.map(todo => (
  <li key={todo.id}>
    {todo.text}
  </li>
))}

mapメソッドで配列の各要素をJSXに変換しています。 key={todo.id}は、Reactが効率的に更新するために必要です。

オブジェクトを扱ってみよう

オブジェクトの状態管理も見てみましょう。

ユーザープロフィール管理

複雑なデータ構造を管理する例として、ユーザープロフィールを作ってみます。

const UserProfile = () => {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: '',
    bio: '',
    preferences: {
      theme: 'light',
      notifications: true,
      language: 'ja'
    },
    skills: []
  });

  const [newSkill, setNewSkill] = useState('');

  // 基本情報の更新
  const updateBasicInfo = (field, value) => {
    setUser(prevUser => ({
      ...prevUser,
      [field]: value
    }));
  };

  // 設定の更新
  const updatePreference = (key, value) => {
    setUser(prevUser => ({
      ...prevUser,
      preferences: {
        ...prevUser.preferences,
        [key]: value
      }
    }));
  };

  // スキルを追加
  const addSkill = () => {
    if (newSkill.trim() && !user.skills.includes(newSkill.trim())) {
      setUser(prevUser => ({
        ...prevUser,
        skills: [...prevUser.skills, newSkill.trim()]
      }));
      setNewSkill('');
    }
  };

  // スキルを削除
  const removeSkill = (skillToRemove) => {
    setUser(prevUser => ({
      ...prevUser,
      skills: prevUser.skills.filter(skill => skill !== skillToRemove)
    }));
  };

  return (
    <div style={{ padding: '20px', maxWidth: '800px' }}>
      <h2>ユーザープロフィール管理</h2>
      
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px' }}>
        {/* 基本情報 */}
        <div>
          <h3>基本情報</h3>
          
          <div style={{ marginBottom: '15px' }}>
            <label>
              名前:
              <input
                type="text"
                value={user.name}
                onChange={(e) => updateBasicInfo('name', e.target.value)}
                style={{
                  width: '100%',
                  padding: '8px',
                  marginTop: '5px'
                }}
              />
            </label>
          </div>
          
          <div style={{ marginBottom: '15px' }}>
            <label>
              メールアドレス:
              <input
                type="email"
                value={user.email}
                onChange={(e) => updateBasicInfo('email', e.target.value)}
                style={{
                  width: '100%',
                  padding: '8px',
                  marginTop: '5px'
                }}
              />
            </label>
          </div>
        </div>
        
        {/* 設定とスキル */}
        <div>
          <h3>設定</h3>
          
          <div style={{ marginBottom: '15px' }}>
            <label>
              テーマ:
              <select
                value={user.preferences.theme}
                onChange={(e) => updatePreference('theme', e.target.value)}
                style={{
                  width: '100%',
                  padding: '8px',
                  marginTop: '5px'
                }}
              >
                <option value="light">ライト</option>
                <option value="dark">ダーク</option>
              </select>
            </label>
          </div>
          
          <h3>スキル</h3>
          
          <div style={{ marginBottom: '15px' }}>
            <input
              type="text"
              value={newSkill}
              onChange={(e) => setNewSkill(e.target.value)}
              placeholder="新しいスキルを追加"
              style={{
                padding: '8px',
                marginRight: '10px'
              }}
              onKeyPress={(e) => e.key === 'Enter' && addSkill()}
            />
            <button 
              onClick={addSkill}
              style={{
                padding: '8px 16px',
                backgroundColor: '#28a745',
                color: 'white',
                border: 'none'
              }}
            >
              追加
            </button>
          </div>
          
          <div>
            {user.skills.map(skill => (
              <span
                key={skill}
                style={{
                  display: 'inline-block',
                  padding: '4px 8px',
                  margin: '2px',
                  backgroundColor: '#007bff',
                  color: 'white',
                  borderRadius: '12px',
                  fontSize: '12px',
                  cursor: 'pointer'
                }}
                onClick={() => removeSkill(skill)}
                title="クリックで削除"
              >
                {skill} ×
              </span>
            ))}
          </div>
        </div>
      </div>
      
      <div style={{ marginTop: '30px', padding: '20px', backgroundColor: '#f8f9fa' }}>
        <h3>プロフィールプレビュー</h3>
        <pre style={{ fontSize: '14px', whiteSpace: 'pre-wrap' }}>
          {JSON.stringify(user, null, 2)}
        </pre>
      </div>
    </div>
  );
};

このコードの重要な部分を見てみましょう。

複雑なオブジェクト構造の初期化です。

const [user, setUser] = useState({
  name: '',
  email: '',
  age: '',
  bio: '',
  preferences: {
    theme: 'light',
    notifications: true,
    language: 'ja'
  },
  skills: []
});

オブジェクトの中にオブジェクトや配列が含まれています。

基本情報の更新方法を見てみましょう。

const updateBasicInfo = (field, value) => {
  setUser(prevUser => ({
    ...prevUser,
    [field]: value
  }));
};

...prevUserで既存のプロパティをコピーしています。 [field]: valueで指定されたプロパティだけを更新しますね。

ネストしたオブジェクトの更新はもう少し複雑です。

const updatePreference = (key, value) => {
  setUser(prevUser => ({
    ...prevUser,
    preferences: {
      ...prevUser.preferences,
      [key]: value
    }
  }));
};

preferencesオブジェクト全体を新しく作り直しています。 既存の設定をコピーして、指定されたキーだけを更新しますね。

これは少し複雑ですが、オブジェクトを直接変更してはいけないというReactのルールを守るために必要です。

よくある間違いと解決方法

useStateを使っていて、よくある間違いとその解決方法を見てみましょう。

状態を直接変更してしまう問題

最もよくある間違いがこれです。

const StateModificationExample = () => {
  const [user, setUser] = useState({ name: '', age: 0 });
  const [items, setItems] = useState([]);

  // ❌ 間違い: 状態を直接変更
  const badUpdate = () => {
    user.name = '新しい名前'; // これは動かない
    user.age = 25; // これも動かない
    // setUserを呼んでいないので再レンダリングされない
  };

  const badAddItem = () => {
    items.push('新しいアイテム'); // これも動かない
    // setItemsを呼んでいないので再レンダリングされない
  };

  // ✅ 正しい: setterを使って更新
  const goodUpdate = () => {
    setUser({
      ...user, // 既存のプロパティをコピー
      name: '新しい名前',
      age: 25
    });
  };

  const goodAddItem = () => {
    setItems([...items, '新しいアイテム']); // 新しい配列を作成
  };

  return (
    <div>
      <h3>状態更新の正しい方法</h3>
      <p>名前: {user.name}</p>
      <p>年齢: {user.age}</p>
      <p>アイテム数: {items.length}</p>
      
      <div>
        <button onClick={badUpdate}>❌ 間違った更新</button>
        <button onClick={goodUpdate}>✅ 正しい更新</button>
      </div>
    </div>
  );
};

間違った例を見てみましょう。

user.name = '新しい名前'; // これは動かない

この書き方では、オブジェクトのプロパティを直接変更しています。 でも、setUserを呼んでいないので、Reactは変更に気づきません。

正しい方法はこちらです。

setUser({
  ...user, // 既存のプロパティをコピー
  name: '新しい名前',
  age: 25
});

スプレッド演算子で既存のプロパティをコピーして、新しいオブジェクトを作成しています。

非同期処理での問題

非同期処理では、また別の問題が起こることがあります。

const AsyncExample = () => {
  const [count, setCount] = useState(0);
  const [loading, setLoading] = useState(false);

  // ❌ 間違い: 古い値を参照する可能性
  const badAsyncUpdate = async () => {
    setLoading(true);
    
    // 複数回クリックされた場合、古いcountの値を使ってしまう
    setTimeout(() => {
      setCount(count + 1); // 古い値を参照
      setLoading(false);
    }, 1000);
  };

  // ✅ 正しい: 関数形式で最新の値を使用
  const goodAsyncUpdate = async () => {
    setLoading(true);
    
    setTimeout(() => {
      setCount(prevCount => prevCount + 1); // 最新の値を使用
      setLoading(false);
    }, 1000);
  };

  return (
    <div>
      <h3>非同期処理での状態更新</h3>
      <p>カウント: {count}</p>
      <p>状態: {loading ? 'ローディング中...' : '待機中'}</p>
      
      <div>
        <button 
          onClick={badAsyncUpdate} 
          disabled={loading}
        >
          ❌ 間違った非同期更新
        </button>
        <button 
          onClick={goodAsyncUpdate} 
          disabled={loading}
        >
          ✅ 正しい非同期更新
        </button>
      </div>
    </div>
  );
};

間違った例を見てみましょう。

setTimeout(() => {
  setCount(count + 1); // 古い値を参照
}, 1000);

この場合、setTimeoutが実行される時点で、countの値が古い可能性があります。

正しい方法はこちらです。

setTimeout(() => {
  setCount(prevCount => prevCount + 1); // 最新の値を使用
}, 1000);

関数形式で書くことで、常に最新の値を使って更新できます。

実践的な活用例

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

会員登録フォーム

フォームの状態管理は、実際の開発でとてもよく使われます。

const RegistrationForm = () => {
  // フォームデータの状態
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    agreeToTerms: false
  });

  // エラーの状態
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // 入力値の更新
  const handleInputChange = (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 !== formData.confirmPassword) {
      newErrors.confirmPassword = 'パスワードが一致しません';
    }
    
    if (!formData.agreeToTerms) {
      newErrors.agreeToTerms = '利用規約に同意してください';
    }
    
    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));
      
      alert('登録が完了しました!');
      
      // フォームリセット
      setFormData({
        username: '',
        email: '',
        password: '',
        confirmPassword: '',
        agreeToTerms: false
      });
    } catch (error) {
      setErrors({ submit: 'エラーが発生しました。もう一度お試しください。' });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div style={{ padding: '20px', maxWidth: '500px' }}>
      <h2>会員登録</h2>
      
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '15px' }}>
          <label>
            ユーザー名 *
            <input
              type="text"
              value={formData.username}
              onChange={(e) => handleInputChange('username', e.target.value)}
              style={{
                width: '100%',
                padding: '10px',
                marginTop: '5px',
                border: `1px solid ${errors.username ? '#dc3545' : '#ddd'}`
              }}
            />
          </label>
          {errors.username && (
            <div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
              {errors.username}
            </div>
          )}
        </div>
        
        <div style={{ marginBottom: '15px' }}>
          <label>
            メールアドレス *
            <input
              type="email"
              value={formData.email}
              onChange={(e) => handleInputChange('email', e.target.value)}
              style={{
                width: '100%',
                padding: '10px',
                marginTop: '5px',
                border: `1px solid ${errors.email ? '#dc3545' : '#ddd'}`
              }}
            />
          </label>
          {errors.email && (
            <div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
              {errors.email}
            </div>
          )}
        </div>
        
        <div style={{ marginBottom: '20px' }}>
          <label>
            <input
              type="checkbox"
              checked={formData.agreeToTerms}
              onChange={(e) => handleInputChange('agreeToTerms', e.target.checked)}
              style={{ marginRight: '8px' }}
            />
            利用規約に同意する *
          </label>
          {errors.agreeToTerms && (
            <div style={{ color: '#dc3545', fontSize: '14px', marginTop: '5px' }}>
              {errors.agreeToTerms}
            </div>
          )}
        </div>
        
        <button
          type="submit"
          disabled={isSubmitting}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: isSubmitting ? '#ccc' : '#007bff',
            color: 'white',
            border: 'none',
            fontSize: '16px'
          }}
        >
          {isSubmitting ? '登録中...' : '登録する'}
        </button>
      </form>
    </div>
  );
};

このコードでは、複数の状態を組み合わせて使っています。

フォームデータ、エラー情報、送信状態を別々に管理していますね。

入力値の更新部分を見てみましょう。

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

汎用的な関数を作ることで、どのフィールドでも同じ関数が使えます。

バリデーション処理も重要なポイントです。

const validateForm = () => {
  const newErrors = {};
  
  if (!formData.username.trim()) {
    newErrors.username = 'ユーザー名は必須です';
  }
  
  setErrors(newErrors);
  return Object.keys(newErrors).length === 0;
};

エラーオブジェクトを作成して、問題があるフィールドだけにエラーメッセージを設定しています。

まとめ:useStateをマスターしよう

useStateについて、基本から実践的な使い方まで詳しく解説しました。

重要なポイントをまとめると

  • useStateはコンポーネントの「記憶力」を提供してくれる
  • 状態が変わると自動的に画面が更新される
  • 配列の分割代入で値と更新関数を受け取る
  • 状態は直接変更せず、必ずsetter関数を使う
  • 配列やオブジェクトは新しいものを作成して更新する

基本的な使い方

  • 数値、文字列、真偽値の管理
  • 配列とオブジェクトの状態管理
  • フォーム入力との連携
  • 条件付きレンダリング

注意すべきポイント

  • 状態の直接変更は避ける
  • 非同期処理では関数形式の更新を使う
  • 複雑な状態は適切に分割する
  • エラーハンドリングも忘れずに

useStateは、React開発の基礎中の基礎です。 この記事で学んだ内容をしっかりと理解することで、より複雑なReactアプリケーションも作れるようになります。

まずは簡単なカウンターから始めて、徐々に複雑な状態管理に挑戦してみてください。

エラーが起きても大丈夫です! この記事の内容を参考にして、正しい使い方を身につけていきましょう。

ぜひ、useStateを使って素敵なReactアプリケーションを作ってくださいね!

関連記事