<CodeLearn/>
React レッスン4

State(useState)

コンポーネントの状態を管理しよう

Stateとは?

Stateは、コンポーネントが内部で保持する「状態」のことです。 ユーザーの操作によって変化するデータ(入力値、開閉状態、カウントなど)をStateとして管理します。

PropsとStateの違いは重要です:

  • Props — 親から渡されるデータ(読み取り専用)
  • State — コンポーネント自身が管理するデータ(変更可能)

Stateが変更されると、Reactはコンポーネントを自動的に再レンダリングしてUIを更新します。

useStateの基本

useState はReactのHookで、 コンポーネントに状態を追加します。

import { useState } from "react";

function Counter() {
  // useState(初期値) → [現在の値, 更新関数] を返す
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>リセット</button>
    </div>
  );
}

// 文字列のState
function NameInput() {
  const [name, setName] = useState("");

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前を入力"
      />
      <p>こんにちは、{name || "ゲスト"}さん!</p>
    </div>
  );
}

// 真偽値のState
function Toggle() {
  const [isOn, setIsOn] = useState(false);

  return (
    <button onClick={() => setIsOn(!isOn)}>
      {isOn ? "ON" : "OFF"}
    </button>
  );
}

関数型更新(prev)

前の状態に基づいて更新する場合は、関数型更新を使うのが安全です。 これにより、古い状態を参照してしまうバグを防げます。

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

  // NG: 連続呼び出しで問題が起きる可能性
  const handleTripleAdd = () => {
    setCount(count + 1);  // countは古い値のまま
    setCount(count + 1);  // 同じ古い値を使う
    setCount(count + 1);  // 結果: +1 しか増えない
  };

  // OK: 関数型更新なら確実
  const handleTripleAddCorrect = () => {
    setCount(prev => prev + 1);  // 最新の値を使う
    setCount(prev => prev + 1);  // 更新後の値を使う
    setCount(prev => prev + 1);  // 結果: +3 増える
  };

  return (
    <div>
      <p>カウント: {count}</p>
      <button onClick={handleTripleAddCorrect}>+3</button>
    </div>
  );
}

prev => パターンは、 前の状態に依存する更新では常に使うことが推奨されます。

オブジェクトのState

Stateにオブジェクトを持つ場合は、スプレッド構文で新しいオブジェクトを作る必要があります。 直接変更(ミューテーション)してはいけません。

function ProfileForm() {
  const [user, setUser] = useState({
    name: "",
    email: "",
    age: 0,
  });

  // NG: オブジェクトを直接変更してはいけない
  // user.name = "太郎";  // Reactが変更を検知できない

  // OK: スプレッド構文で新しいオブジェクトを作る
  const updateName = (newName) => {
    setUser({ ...user, name: newName });
  };

  // ネストしたオブジェクトの更新
  const [profile, setProfile] = useState({
    name: "太郎",
    address: {
      city: "東京",
      zip: "100-0001",
    },
  });

  const updateCity = (newCity) => {
    setProfile({
      ...profile,
      address: {
        ...profile.address,
        city: newCity,
      },
    });
  };

  return (
    <form>
      <input
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
        placeholder="名前"
      />
      <input
        value={user.email}
        onChange={(e) => setUser({ ...user, email: e.target.value })}
        placeholder="メール"
      />
      <p>名前: {user.name}、メール: {user.email}</p>
    </form>
  );
}

配列のState

配列も同様に、直接変更せず新しい配列を作ることが重要です。

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "買い物", done: false },
    { id: 2, text: "掃除", done: true },
  ]);
  const [input, setInput] = useState("");

  // 追加(スプレッド構文)
  const addTodo = () => {
    if (!input.trim()) return;
    setTodos([...todos, {
      id: Date.now(),
      text: input,
      done: false,
    }]);
    setInput("");
  };

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

  // 更新(map)
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id
        ? { ...todo, done: !todo.done }
        : todo
    ));
  };

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="新しいTODO"
      />
      <button onClick={addTodo}>追加</button>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              onClick={() => toggleTodo(todo.id)}
              style={{
                textDecoration: todo.done ? "line-through" : "none",
                cursor: "pointer",
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

配列操作の鉄則:pushsplice は使わず、filtermap、スプレッド構文で新しい配列を作りましょう。

まとめ

  • useState でコンポーネントに状態を追加する
  • State更新でReactが自動再レンダリングする
  • 前の状態に基づく更新は関数型更新(prev =>)を使う
  • オブジェクト・配列は直接変更せず、新しいコピーを作る