こんにちは!フロントエンドの開発現場で、日夜コードと向き合っているシニアエンジニアです。
Reactを触り始めると、必ずと言っていいほど出会う奇妙なルールがありますよね。そう、「Reactのフック(`useState`や`useEffect`など)は、コンポーネントのトップレベルで呼び出さなければならない。ループや条件分岐、ネストした関数内で呼び出してはいけない」という鉄の掟です。
「なんでループさせちゃダメなの? 毎回同じ順番で呼べばいいだけじゃない?」
そんな疑問を持ったことはありませんか?
実はこのルール、Reactの優しさでも何でもなく、JavaScriptという言語の心臓部、そしてV8エンジンが裏側で行っている「メモリ管理と実行コンテキストの仕組み」に深く根ざした必然なんです。
ここをクリアすれば、JavaScriptの変数管理の仕組み(スコープと配列の紐付け)が一気に腑に落ちて、あなたのコードは一段と洗練されますよ。さあ、一緒にその裏側の世界を覗いてみましょう!
—
1. そもそもReactのフックはどうやって動いているのか?(裏側の秘密)
JavaScriptのコードがブラウザで実行されるとき、V8などのエンジンは「実行コンテキスト(Execution Context)」というメモリ空間を作り、変数を管理します。
しかし、Reactの`useState`は、JavaScriptの言語仕様そのものではありません。Reactというライブラリが提供する「状態(State)を記憶するための仕組み」です。
ここで、プログラミング初心者の多くが勘違いしがちなポイントがあります。
「`useState(0)`って書くと、その変数(例えば `count`)が自分自身の状態をどこかに持っているんでしょ?」
実は、違います。
Reactは、コンポーネントが何個のstateを持っているかを、「ただの配列(あるいは連結リスト)」として順番に記憶しているだけなのです。
イメージしてみましょう。あなたの脳内やメモ帳に、こんな配列があると思ってください。
// Reactが内部でこっそり管理している「記憶のメモ帳(配列)」のイメージ
const memoizedStates = [0, “Taro”, false];
コンポーネントが描画(レンダリング)されるたびに、Reactはこのメモ帳の先頭から順番に、「あ、1番目はカウント用の数字ね」「2番目はユーザー名ね」と、インデックス(番号)で値を取り出しています。
—
2. 【実例】なぜループや条件分岐で呼んではいけないのか?
では、もしここに「条件分岐」や「ループ」が入り込んできたらどうなるでしょうか?
ここに、Reactのフックにおける最大の罠(バグの温床)があります。
以下の「やってはいけないコード」を見てください。
import React, { useState } from ‘react’;
function BadComponent({ isDarkMode }) {
// ⚠️ 禁忌:条件分岐の中でフックを呼んでいる!
let themeColor = ‘white’;
if (isDarkMode) {
const [darkTheme, setDarkTheme] = useState(‘black’); // 条件によって呼ばれたり呼ばれなかったりする
themeColor = darkTheme;
}
const [count, setCount] = useState(0); // いつもは2番目に呼ばれるはず…?
return (
カウント: {count}
);
}
このコードの何がヤバいのか、Reactの「記憶のメモ帳」の目線になってシミュレーションしてみましょう。
パターンA:初回レンダリング時(`isDarkMode = true` の場合)
1. `if (isDarkMode)` が「真」なので、`useState(‘black’)` が実行されます。
👉 メモ帳の 1番目 に ` ‘black’ ` が登録されます。
2. その後、`useState(0)` が実行されます。
👉 メモ帳の 2番目 に ` 0 ` が登録されます。
- 📝 メモ帳の状態: `[‘black’, 0]` (完璧ですね)
パターンB:2回目以降のレンダリング(もし親の気まぐれで `isDarkMode = false` に変わったら?)
1. `if (isDarkMode)` が「偽」になるため、`useState(‘black’)` はスキップ(実行されない)されます。
2. 次に、下にある `useState(0)` が実行されます。
👉 Reactはこれを「いつもの2番目のフックだな」と思ってメモ帳を見に行きますが、今回の実行では1番目をスキップしたため、メモリのインデックスがズレてしまいます!
結果として、Reactは「あれ? 本来 `count` が入っているはずの場所に、別のデータが入っているぞ!?」とパニックを起こし、画面の表示がおかしくなったり、最悪の場合は致命的なクラッシュ(無限ループや型エラー)を引き起こすのです。
Reactは、変数の「名前(例: `count`)」を見て状態を覚えているのではなく、「呼ばれた順番(インデックス)」だけで状態を管理している。だからこそ、宣言の順番が絶対的に狂ってはならないのです。
—
3. ループ内でフックを使いたくなったらどうする?
「でも、リストの数だけ動的にステートを持ちたい時だってあるよ!」
そんなときはどうすればいいでしょうか?
JavaScriptの配列メソッドや、データ構造そのものを工夫するのがプロの技の見せ所です。フックをループさせるのではなく、「フックの呼び出し回数は常に一定にしつつ、中身のデータ構造を配列やオブジェクトにする」というアプローチを取ります。
悪い例(フックをループさせる)
// ❌ 絶対にNG
function BadList({ items }) {
return (
-
{items.map(item => {
- {item.name}
const [isChecked, setIsChecked] = useState(false); // ループ内でフック!
return
;
})}
);
}
正しい例(状態自体を1つのオブジェクトや配列として持つ)
import React, { useState } from ‘react’;
// ✅ フックの呼び出しは常にトップレベルで1回だけ行う
function GoodList({ items }) {
// アイテムごとのチェック状態を、IDをキーにしたオブジェクトで管理する
const [checkedMap, setCheckedMap] = useState({});
const toggleCheck = (id) => {
setCheckedMap(prev => ({
…prev,
[id]: !prev[id] // 該当するIDの状態だけを反転させる
}));
};
return (
-
{items.map(item => (
-
toggleCheck(item.id)}
/>
{item.name}
))}
);
}
これなら、`useState` 自体はコンポーネントのトップレベルで毎回確実に1回だけ呼ばれます。そのため、Reactの記憶のメモ帳がインデックス迷子になることは絶対にありません。
—
まとめ:ここをクリアすれば、Reactの挙動が手に取るようにわかる!
いかがでしたでしょうか?
- Reactのフックは、変数名ではなく「呼び出された順番(インデックス)」で内部の値を記憶している。
- だからこそ、条件分岐やループの中でフックを呼ぶと、順番が狂ってV8エンジンやReactのメモリ管理が崩壊する。
- 動的なデータを扱いたいときは、フックを増減させるのではなく、管理するデータ構造(オブジェクトや配列)のほうを工夫する。
この原則さえ頭に叩き込んでおけば、ReactのStateにまつわる不可解なバグの9割は未然に防ぐことができます。
「なぜそのルールが存在するのか?」という言語やフレームワークの裏側の仕組み(ランタイムの都合)まで理解してコードを書くエンジニアは、現場でも圧倒的に重宝されます。ここをクリアしたあなたなら、もうフックで迷うことはありません。
明日からのコーディングで、ぜひこの知見を活かしてみてくださいね。それでは、また次の深淵でお会いしましょう!