Hooksは内部でどう動いているのか?
なぜHooksは条件分岐の中で使えないのか。useStateのstateはどこに保存されているのか。Reactの内部実装で理解します。
🎯 結論
HooksのstateはFiberオブジェクトに連結リストとして保存されています。
Reactは「何番目のHookか」でstateを管理するため、Hooksの呼び出し順が変わると間違ったstateが返ってしまいます。
🔍 よくある疑問:なぜ条件分岐でHooksを使えないのか?
function Component({ show }) {
if (show) {
// ❌ 条件分岐の中でHook
const [x, setX] = useState(0);
}
const [y, setY] = useState('');
// ...
} function Component({ show }) {
// ✅ 常にトップレベルで
const [x, setX] = useState(0);
const [y, setY] = useState('');
// showによる処理は中で行う
if (!show) return null;
// ...
} Reactのエラーメッセージ「Hooks must be called in the same order」が示す通り、Hooksは必ず同じ順番で呼ばれる必要があります。その理由が内部実装にあります。
🧠 Stateの保存場所:Fiberの連結リスト
各コンポーネントに対応するFiberオブジェクトにはmemoizedStateというフィールドがあります。
ここにHookのstateが連結リスト(linked list)として保存されます。
// Fiberオブジェクト(簡略版)
{
type: MyComponent,
memoizedState: { // ← 1番目のHook (useState(0))
memoizedState: 0, // 現在の値
queue: UpdateQueue, // 更新キュー
next: { // ← 2番目のHook (useState(''))
memoizedState: '',
queue: UpdateQueue,
next: { // ← 3番目のHook (useEffect)
memoizedState: [deps],
next: null
}
}
}
} なぜ順番が重要か
Reactは「このコンポーネントの1番目のuseState」「2番目のuseState」という形でstateを管理します。 レンダリングのたびにHookを順番に呼ぶことで、連結リストのノードと1対1で対応させています。 条件分岐でHookをスキップすると、「2番目のuseState」が実は「前回の3番目のuseState」と対応してしまいます。
⚙️ useStateの内部:更新キュー
setStateを呼ぶと、
Reactはすぐにstateを変えるのではなく、更新を「キュー」に積みます。
// setCount(prev => prev + 1) を3回呼んだ場合
UpdateQueue: {
updates: [
{ action: prev => prev + 1 }, // 1回目
{ action: prev => prev + 1 }, // 2回目
{ action: prev => prev + 1 }, // 3回目
]
}
// 次のrenderで全部処理されて +3 になる バッチ処理(batching)
React 18では、イベントハンドラ内の複数のsetStateは自動でまとめて(バッチ)処理されます。 3回setStateを呼んでも再レンダリングは1回で済みます。 これもキューの仕組みがあるから実現できます。
📋 useEffectの内部:依存配列の比較
useEffectのHookノードには現在の依存配列が保存されています。 再レンダリング時にReactは前回の依存配列と今回を比較し、変化があった場合にのみeffectを再実行します。
// useEffectの依存比較(内部の疑似コード)
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) return false;
for (let i = 0; i < prevDeps.length; i++) {
// Object.is で比較(===とほぼ同じ)
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false; // 変化あり → effect再実行
}
return true; // 変化なし → effect スキップ
} オブジェクト・関数は毎回「変化あり」になる
Object.isは参照比較なので、
毎回新しく作られるオブジェクト({})や関数(() => {})は
内容が同じでも「変化あり」とみなされます。
これがuseMemoやuseCallbackが必要になる理由です。
🔄 初回マウントと再レンダリングの切り替え
Reactは内部でHookの「マウント用」と「更新用」の2セットの実装を持っています。
// Reactの内部(簡略版)
const HooksDispatcherOnMount = {
useState: mountState, // 初回: 新しいHookノードを作る
useEffect: mountEffect,
};
const HooksDispatcherOnUpdate = {
useState: updateState, // 更新: 既存のHookノードを使う
useEffect: updateEffect,
};
// ReactCurrentDispatcher.current が
// マウント時と更新時で切り替わる 📌 まとめ
- ✓ HooksのstateはFiberオブジェクトの連結リストに保存される
- ✓ ReactはHookを「何番目か」で識別するため、呼び出し順が変わると誤動作する
- ✓ setStateは「更新キューに積む」処理——即座に値が変わるわけではない
- ✓ useEffectの依存配列はObject.is(参照比較)で比較される
- ✓ オブジェクト・関数は毎回新しい参照になるためuseMemo/useCallbackが必要になる