Reactで誰もがやるやつ。
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log("setStateの直後:", count); // 更新後の値が見たい
};
return (
<div>
<p>count: {count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
ボタン1回。結果。
setStateの直後: 0
count: 1 で setCount を呼んだのに、直後の count は変わってない。
すぐに変わるわけじゃない setState
setCount(count + 1)はcountを書き換える命令ではない。 「次の render ではcountを1にしてくれ」と React に予約するだけ。今動いている関数の中のcountはその render 時点の snapshot なので、予約しても書き換わらない。
useState宣言はこう。
const [count, setCount] = useState(0);
左辺の [count, setCount] は配列ではなく分割代入。useState が返す配列 [0, 関数] を分解して、0番目を count、1番目を setCount に入れているだけ。だから count の中身はただの const 数値 0。
数値を const で束縛したら、この関数の実行中に変わりようがない。JavaScriptとして当たり前。
count は「今の状態を指す変数」ではなく 「その render 専用の定数」。setCount は次の render 用に値を予約するだけ。
新しい値を使いたいときは
更新後の値を今すぐ使いたいときは?
1. 自分で計算した値をそのまま使う
setCount(count + 1) を呼ぶ時点で、count + 1 は自分で計算している。だったらその値を変数に持てばいい。
const handleClick = () => {
const next = count + 1;
setCount(next);
console.log("次のcount:", next);
};
次のcount: 1
「setCount のあとに count を読もう」ではなく「次の値は自分で持っておく」。
2. setState に関数を渡して更新する
もう一つは setCount(prev => prev + 1)。
値ではなく「今の値を受け取って次の値を返す関数」を渡す。Reactが最新のstateを引数に渡してくれる
prev はどこかで宣言済みの変数ではない。この場で作った関数の引数名。分解するとこう。
const updater = (prev) => prev + 1; // 前の値を受け取って +1 を返すだけの関数
setCount(updater); // 値ではなく関数を渡す
この関数を呼ぶのは自分ではなく React。予約を消化するとき その時点の最新 state を prev に入れて呼んでくれる 。引数名なので prev でも c でも何でもいい。
違いは setCount を連続で呼んだとき。値をそのまま入れると:
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
3増えると思きや1しか増えない。
count: 1
理由。3回の setCount(count + 1) はどれも同じ count(= 0)を参照している。0 + 1 を3回予約しているだけ。だから 1。
関数形式で同じことを試す。
const handleClick = () => {
setCount(prev => prev + 1); // prev = 0 -> 1 になる
setCount(prev => prev + 1); // prev = 1 -> 2 になる
setCount(prev => prev + 1); // prev = 2 -> 3 になる
};
count: 3
こっちは3増える。関数形式は 前回の予約結果 を prev として受け取れる。
つまり関数形式が要るのは、直前のstateに依存して次のstateを決めるとき。単純な代入(setName("bob")など)ならどっちでも結果は変わらない。
本当に「更新後の値」を見たいとき
state更新後の状態で何かしたいなら useEffect で拾う
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("countが変わった:", count);
}, [count]);
return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}
ボタン2回。
countが変わった: 0
countが変わった: 1
countが変わった: 2
useEffect はその処理を render 後に走らせる仕組み。
React の1回の render は「Render phase(component 関数を呼んで次の画面を計算する)」と「Commit phase(計算結果を DOM に反映する)」に分かれている。useEffect の処理が走るのは Commit のさらに後(Passive Effects)。この粒度で並べるとこう。
分割のポイント。count が 1 に確定するのは「re-render が始まる前」ではなく、Render phase で useState(0) が呼ばれた瞬間。ここでuseStateはそれまでに queue されていた予約を実行して count を更新する。
useState(0)で 0 を入れているのに、これはあくまで default valueなだけで、その前のrenderで count の値の変更が予約されているため default value 0 は使われない。だから component 関数の実行中に count の値が動くことはなく、各 render の中では常に定数。
「setCount の直後に何かしたい」は、たいてい「stateが更新されたら何かしたい」に言い換えられる。前者は無理。後者は useEffect で書ける。
補足:境界
- ここで書いた挙動は、少なくとも確認した範囲(React 18 /
useState)でのもの。React 17以前と18以降では event handler 外の batching 挙動が違う。 -
useRefは逆に「その場で書き換わる」入れ物。ただし変えても re-render は起きない。値は保持したいが画面には関係ない、というケース向け。 - 上の説明はClaude Opus 4.8に教わった内容を、自分で動かして確認したもの。仕様の一次情報はReact公式ドキュメント。記事末尾にリンク。
わかったこと
setState は「今の値を変える」命令ではなく 「次の render 用に値を予約する」命令。
-
setCount(x)はその場でcountを書き換えない。次の render 用の予約。 - countが変わるのは次のrender時。何度
setCountを呼んでも re-render は次の1回に 畳み込まれる(setCountを3回ならその3回を順にやった後に次のrenderが走る) - 今の render 中の
countはその render 専用の snapshot。あとから読んでも古い。constということを忘れない
参考
- React docs — useState
- React docs — State as a Snapshot
- React docs — Queueing a Series of State Updates