useState基本仕様とReactの更新モデル
この記事の目的
Reactの「更新モデル」を体系的に理解すること。
useStateの基本仕様を踏まえ、次の3点を結びつけて、UIがいつ・どのように更新され、stateがどのタイミングで反映されるかを理解します。
- レンダーとコミットの流れ(トリガ → レンダー → DOMコミット)
- stateは各レンダーで固定される“スナップショット”という性質
- イベント中の複数setStateが“バッチ処理”で次レンダーに一括適用される仕組み
1. useStateの基本仕様
1-1. 通常の変数ではUIが更新されない理由
- ローカル変数はレンダー間で保持されない。
- ローカル変数の変更では再レンダーがトリガされない。
1-2. useStateが提供するもの
- レンダー間で値を保持する“state変数”。
- 値を更新して再レンダーを予約する“セッタ関数”。
- 記法例:const [value, setValue] = useState(initial)
1-3. フックは「呼び出し順」に依存
-
同一コンポーネントの各レンダーで、フックは同じ順序で呼ぶ(トップレベルで無条件に)。条件分岐・ループ・ネスト関数内で呼ぶと順序が崩れて不整合。
※Reactは各コンポーネントに「フックの並び」を持ち、レンダーごとに1番目→2番目→…の順で割り当てているため、条件で呼ぶ・呼ばないがあると、この並びがズレて破綻する。 -
脳内モデル:Reactはコンポーネントごとに“フック状態の配列”と“現在インデックス”を持ち、呼び出し順で対応付ける。
詳細:https://ja.react.dev/learn/state-a-components-memory#giving-a-component-multiple-state-variables
1-4. インスタンスごとにstateは独立・プライベート
- 同じコンポーネントを2つ並べても、それぞれのstateは別々に保持され、片方の更新はもう片方に影響しない。
2. UIがいつ・どう更新され、stateはいつ反映される?
以下のフロー図は、イベントからレンダー、コミットまでの全体像です。
2-1. レンダーとコミットの流れ(トリガ → レンダー → DOMコミット)
- トリガ
- 初回レンダー、またはstate更新(set関数)で再レンダーが“予約”される。
- レンダー
- Reactがコンポーネント関数を「呼び出し」、その瞬間のUI(JSX)を“純粋計算”で作る工程。同じ入力なら同じ出力。ここではDOMを触らない。
- コミット
- レンダー結果の差分のみをDOMに適用。必要最小限の変更で画面が更新される。
2-2. stateは各レンダーで固定される“スナップショット”
- setStateしても“その場”では値は変わらない。反映は“次回のレンダー”。
なぜスナップショットかというと
setStateは「今すぐ値を書き換える」ではなく「次のレンダーを予約する」動作だからです。
予約が実行されて初めて“次の一枚”が撮られ、そこで新しいstateが反映されます。
だから同じレンダー内で何度setStateしても、ハンドラ内から見えるstateは変わらないし、setTimeoutなど非同期でも、コールバックは“撮影時の一枚”に写っていた値を持ち続けます。
以下の例は、最初のレンダーでは number は 0 のスナップショットです。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
function handleClick() {
// このハンドラが「作られたレンダー」の number を見ています(今回0)
setNumber(number + 1); // 0 + 1 をキューへ
setNumber(number + 1); // 0 + 1 をキューへ
alert('handler内のnumberは:' + number); // 0 と表示される
}
setTimeout(() => {
// 非同期でも「作成時のレンダー」のnumberを保持
alert('setTimeout内のnumberは:' + number); // 0 と表示される
}, 1000);
return (
<>
<h1>{number}</h1>
<button onClick={handleClick}>+2</button>
</>
);
}
設計指針
- 「レンダー単位のスナップショット」で思考すること。
→ ハンドラ内部で“今見えている値”に依存する連続更新は誤差に注意。
2-3. イベント中の複数setStateは“バッチ処理”で一括適用
- Reactはイベントハンドラが終わるまで、複数のsetStateをキューにまとめる(バッチ処理)。
- ハンドラ終了後に再レンダーが走るため、UIは“途中の状態”を見せない。
正しい積み上げ(更新用関数)
// onClick内の更新キュー:
setNumber(n => n + 1); // n=0 => 1
setNumber(n => n + 1); // n=1 => 2
setNumber(n => n + 1); // n=2 => 3
// → 次レンダーで number=3
置き換えと更新用関数の混在ルール
setNumber(number + 5); // "5に置き換えよ"
setNumber(n => n + 1); // 5 => 6
setNumber(42); // "42に置き換えよ"(最終結果)
// → 次レンダーで number=42
補足
- 意図的なイベント(クリック等)が“複数回”発生した場合、それらはイベントごとに別のバッチで処理される。イベント間のキューの合体は起きない。
- 更新用関数は純関数。副作用や別のsetStateを内部で行わない。
3. 更新モデルを理解することの重要性
Reactの更新モデル(スナップショット/バッチ処理/レンダーとコミット)を誤解すると、予測不可能なバグに遭遇するかもしれません。
各項目に具体例と正しい対処を示します。
3-1. 連続更新が積み上がらない(最後の置き換えだけが効く)
同じレンダー内で古い値(スナップショット)に依存して加算すると、最後の“置き換え”だけが有効になり、結果が1しか増えないなどの上書きが起きます。
- 問題例
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// このレンダーで見える count は固定(例: 0)
setCount(count + 1); // 0+1 を予約
setCount(count + 1); // 0+1 を予約(上書き)
setCount(count + 1); // 0+1 を予約(さらに上書き)
};
return <button onClick={handleClick}>+3</button>;
}
// → 次レンダーで count は 1 のみ
- 正しい対処(更新用関数で直前の確定値を基準に積み上げる)
const handleClick = () => {
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
};
// → 次レンダーで count は 3
3-2. フック呼び出し順の破壊(条件分岐内の useState でクラッシュや取り違え)
条件分岐やループ内でフックを呼ぶと、レンダー間で呼び出し順が変わり、Reactの内部対応付けが壊れて状態が取り違えられます。
- 問題例
function Comp({ flag }) {
if (flag) {
const [a, setA] = useState(0); // NG: 条件内のフック
}
const [b, setB] = useState(0);
// flagの変化で a/b のスロットがズレて不整合
}
- 正しい対処(トップレベルで常に同順序)
function Comp({ flag }) {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
// 条件分岐は「使うかどうか」を分岐し、フック自体は常に呼ぶ
return flag ? <A value={a} /> : <B value={b} />;
}
以上のバグは、次の前提を守ることで回避できます。
- レンダーは「スナップショット(その時点の state/props)」で純粋計算される。
- イベント内の複数更新は「バッチ処理」で、次のレンダー開始時にまとめて適用される。
- 実際のDOM変更は「コミット」で差分適用される。
まとめ
- トリガ→レンダー→コミットの三段階と、レンダーは“純粋計算”である性質を理解する。
- stateは各レンダーで“スナップショット”。同一レンダー中の値は固定、反映は“次のレンダー”。
- 複数setStateはイベント終了まで“バッチ処理”でキューに入り、次のレンダーで一括適用される。
- この前提に立てば、意図通りのUI更新と、バグの少ない状態管理を設計できる。
実務では、連続/非同期更新は常に「更新用関数(prev => …)」を使い、フックはトップレベルで同順序、そして副作用はレンダーの外(イベントハンドラやエフェクト)に置く、という基本を徹底すると安全です。
Discussion