useState — stateの仕組みと参照の罠
前章までで、JSX・コンポーネント・propsというReactの骨格と、レンダー・マウントのライフサイクルを見てきました。
この章からはHooksに入ります。最初に扱うのは、もっとも頻繁に使う useState です。
学習者普通の変数に代入しても画面が変わらないのに、useState だと変わるのはなんで?
UIはstateの関数
Reactの根本的な考え方は UI = f(state) です。
stateが変われば自動的にUIが更新される。
つまり
「DOMを直接操作するのではなく、stateを変えることで間接的にUIを制御」という考え方がReactの基本です。
コンポーネントが扱うデータにはpropsとstateの2種類があります。
propsは親コンポーネントから渡される読み取り専用のデータで、 関数の引数 に相当します。 一方stateはコンポーネント自身が管理・変更できるデータで、変更するとそのコンポーネントが再レンダリングされます。
useStateの基本
useState は、コンポーネントに状態変数を追加するフックです。
値を更新するとReactが再レンダーをスケジュールし、新しい値に基づいてUIが再計算されます。
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount((c) => c + 1)}>
カウント {count}
</button>
);
}stateの正体 — 関数が値を覚えられる理由
普通の関数は呼び出しが終わればローカル変数は消えます。
しかし useState で管理した値は、関数が再実行されても前回の値を引き継げます。
これは、stateがコンポーネント関数自体に保持されているわけではないからです。
実態はReactのランタイムが内部のデータ構造(Fiberツリー)にstateを保管しており、useState はその保管場所へのアクセス手段です。
set関数への値の渡し方
次のstateを直接渡す
setCount(count + 1);シンプルだが、同一レンダー内で複数回呼ぶと count が古い値を参照する場合があります。
function handleTripleClick() {
setCount(count + 1); // count = 0 → 1
setCount(count + 1); // count = 0 → 1(同じレンダーなのでcountはまだ0)
setCount(count + 1); // count = 0 → 1(同上)
// 結果: 1回しか増えない
}更新用関数(updater function)を渡す
setCount((prev) => prev + 1);setCount に関数を渡すと、Reactがその関数を呼び出すときに「現在のstateの値」を第1引数として自動的に渡してくれます。
prev の中身はReactが入れてくれるもので、自分が書くのは「前の値をもらったら何を返すか」のロジックだけです。
function handleTripleClick() {
setCount((prev) => prev + 1); // 0 → 1
setCount((prev) => prev + 1); // 1 → 2
setCount((prev) => prev + 1); // 2 → 3
// 結果: 3回増える
}迷ったらupdater functionを使えば安全です。
オブジェクトや配列のstateと参照の罠
学習者user.name = '新しい名前' ってstateを直接書き換えたのに、画面がまったく変わらない…バグ?
先生バグじゃなくて、Reactの「変化の検知のしかた」が原因。鍵は 参照 だよ。なぜそうなるか順番に見てみよう。
数値や文字列のstateは問題なく更新できたのに、オブジェクトや配列にすると動かない、というケースがあります。 原因はJavaScriptの「参照」の仕組みにあります。
参照比較の仕組み
JavaScriptでは、数値や文字列は値そのものが比較されますが、オブジェクトや配列は「メモリ上のどこにあるか(参照)」で比較されます。
// 数値の比較 → 値が同じなら等しい
1 === 1; // true
// オブジェクトの比較 → 同じ参照でないと等しくない
const a = { name: 'Alice' };
const b = { name: 'Alice' };
a === b; // false(中身は同じだが、別のオブジェクト)Reactのset関数は、渡された値が前回と同じかどうかを Object.is(ほぼ === と同じ)で判定します。
オブジェクトのプロパティを書き換えても、オブジェクト自体は同じ参照のままなので、Reactは「変わっていない」と判断して再レンダーをスキップします。
オブジェクトのstateを更新する
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// NG — プロパティを書き換えても参照は同じ
user.name = 'Bob';
setUser(user); // Reactは変化を検知しない
// OK — スプレッド構文で新しいオブジェクトを作る
setUser({ ...user, name: 'Bob' });{ ...user, name: 'Bob' } は「user のすべてのプロパティをコピーした新しいオブジェクトを作り、name だけ上書きする」という意味です。新しいオブジェクトなので参照が変わり、Reactは変化を検知します。
配列のstateを更新する
配列も同様です。push や splice は元の配列を変更する(ミューテーション)ので、参照が変わりません。
const [items, setItems] = useState(['React', 'Next.js']);
// NG — pushは元の配列を変更するだけ
items.push('Remix');
setItems(items);
// OK — スプレッド構文で新しい配列を作る
setItems([...items, 'Remix']);配列操作ごとに使うべきメソッドが異なります。
追加には [...items, newItem]、削除には items.filter(item => item.id !== targetId)、変更には items.map(item => item.id === targetId ? { ...item, done: true } : item) を使います。いずれも元の配列を変更せず、新しい配列を返すメソッドです。
ネストしたオブジェクトの更新
ネストが深いと、スプレッド構文が冗長になります。
const [form, setForm] = useState({
name: 'Alice',
address: {
city: 'Tokyo',
zip: '100-0001',
},
});
// cityだけ変更する
setForm({
...form,
address: {
...form.address,
city: 'Osaka',
},
});ネストが3階層以上になるとコードが読みにくくなります。その場合はstateの構造をフラットにするか、Immerなどのライブラリを検討してください。
複数のstateを分けるか、1つにまとめるか
コンポーネントが複数の値を管理する場合、分け方には2つのアプローチがあります。
// 分ける — それぞれ独立して更新する場合に向く
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [isEditing, setIsEditing] = useState(false);
// まとめる — 常にセットで更新する場合に向く
const [form, setForm] = useState({ name: '', age: 0 });判断基準はシンプルで、「その値は常に一緒に更新されるか」です。 一緒に更新されるなら1つのオブジェクトにまとめた方が、set関数の呼び出しが1回で済み、更新漏れも防げます。 独立して更新されるなら分けた方が、イミュータブル更新が楽で、コードも読みやすくなります。
stateの設計が複雑になったら — useReducerの存在
stateの更新パターンが増えてきたら(追加・削除・編集・リセットなど)、useReducer という選択肢があります。
// useReducerのイメージ(詳細は別途)
const [state, dispatch] = useReducer(reducer, initialState);
// 「何をするか」をdispatchで伝える
dispatch({ type: 'ADD_ITEM', payload: newItem });
dispatch({ type: 'DELETE_ITEM', payload: itemId });useReducer は本書のスコープ外ですが、「stateの更新ロジックが複雑になったらuseReducerに切り出せる」ということだけ覚えておいてください。
まとめ
useState はUIに影響するローカル状態の置き場所です。stateが変わるとコンポーネントが再レンダーされ、UIが新しい値に基づいて再計算されます。
stateの値はコンポーネント関数自体ではなくReactのFiberツリーに保管されるため、関数が再実行されても値を引き継げます。
オブジェクトや配列をstateに持つ場合は、プロパティを直接変更するのではなく、スプレッド構文や map・filter で新しいオブジェクト・配列を作って渡す必要があります。Reactは参照の同一性で変化を判定するためです。