こんにちは!日々のフロントエンド開発、本当にお疲れ様です。
Reactを使ったコンポーネント設計をしていると、ふとこんなルールを聞いたことがありませんか?
「Reactのフック(`useState` や `useEffect` など)は、ループや条件分岐の中で呼び出してはいけない」
公式ドキュメントにもサラッと書いてあるこのルールですが、「えっ、なんで? 普通に変数と同じようにループ内で使いたくなることもあるのに……」って思いませんでしたか?
ここをクリアできるかどうかが、JavaScriptのランタイムの動きと、Reactの内部構造の本質を理解しているかどうかの分かれ道になります。今回は、スコープとJavaScriptのメモリ管理の視点から、この「Reactの罠」を一緒に完全に解き明かしていきましょう!ここさえ分かれば、あなたのJavaScript力は一段とグッと上がりますよ。
—
そもそも、Reactのフックってどうやって動いているの?
私たちが普段何気なく使っている `useState` ですが、JavaScriptのエンジン(V8など)から見ると、これはただの「関数」です。では、コンポーネントが何回も再描画(レンダリング)されるとき、Reactは一体どうやって「どの状態がどの変数に対応しているのか」を覚えているのでしょうか?
実は、Reactは「フックが呼び出された順番(順番のインデックス)」だけで状態を管理しています。
名前やキーで管理しているわけではありません。Reactの内部では、コンポーネントごとに「フックの呼び出し結果を順番に並べた配列(リンクリスト)」が用意されています。
イメージとしては、こんな感じです。
【React内部のメモリー空間のイメージ】
コンポーネントAの初回の描画時:
[ 1個目のフック (useState: count) ] -> 順序: 0番目
[ 2個目のフック (useEffect) ] -> 順序: 1番目
Reactは、「お、今回は上から数えて0番目だから `count` ね、1番目だから `useEffect` ね」という風に、純粋に呼び出される順番(コールオーダー)だけを頼りにメモリ上のデータを引いているのです。
—
ループや条件分岐の中でフックを使うと、なぜ壊れるのか?
ここで、今回の本題である「ループや条件分岐の中でフックを宣言してはいけない理由」に直結します。
もし、条件によって実行されたりされなかったりする場所(`if` 文の中や `for` ループの中)でフックを呼んでしまったらどうなるでしょうか? 実際のコード例を見てみましょう。
🚨 やってはいけない「バグる」コード例
import React, { useState } from ‘react’;
function BadCounter({ isHappy }) {
// ⚠️【アンチパターン】条件分岐の中でフックを呼んでいる!
if (isHappy) {
var [message, setMessage] = useState(‘やったね!’);
}
const [count, setCount] = useState(0);
return (
カウント: {count}
);
}
このコード、何が問題だか分かりますか? JavaScriptのスコープや変数宣言(`var` や `let`)の問題ではなく、「Reactが記憶している順番と、実際の実行順序がズレてしまう」という致命的なバグを引き起こします。
メモリのズレ(インデックスの破壊)を追ってみる
もし親コンポーネントの気まぐれで、`isHappy` が `true` から `false` に変わったとします。
- 初回描画時 (`isHappy = true`):
1. `useState(‘やったね!’)` が呼ばれる(0番目)
2. `useState(0)` が呼ばれる(1番目)
- 2回目以降の描画時 (`isHappy = false`):
1. `if (isHappy)` の中に入らないため、1つ目の `useState` がスキップされる!
2. その結果、次に呼ばれた `useState(0)` が、Reactからは 「0番目のフックが呼ばれた」 と誤認されてしまう。
これにより、Reactは「あれ? 前回は0番目に `message` のデータがあったのに、今回は急に `count` のデータになってるぞ! 型が合わない!」と混乱し、最悪の場合はアプリケーションがクラッシュするか、全く意図しない別の変数の値が画面に表示されるという、デバッグ泣かせの怪奇現象が起きます。
これが、ループや条件分岐のスコープ内でフックを呼んではいけない、唯一にして最大の理由です。Reactは「名前」ではなく「順番」で状態を覚えているからこそ、その順番が絶対に狂ってはならないのです。
—
スコープの観点から見る「正しい変数」と「フック」の違い
ここで、JavaScriptの「変数スコープ(Variable Scope)」と「Reactのフック」の決定的な違いについても整理しておきましょう。
JavaScriptの通常の変数(`let` や `const`)は、ブロックスコープ(`{}` の中)に閉じ込められます。
function NormalFunction() {
if (true) {
let tempValue = ‘ローカル変数’; // この変数はif文の外からは見えない
console.log(tempValue);
}
// ここでは tempValue はスコープ外なので参照できない(ReferenceError)
}
JavaScriptの変数は、ブロックスコープや関数スコープによって安全にカプセル化され、スコープを抜ければメモリからガベージコレクションの対象になります。
しかし、Reactのフックは、スコープの内側にあろうがなかろうが、実行された「順番」で大元の配列にパズルのピースのようにカプセル化されていきます。 スコープのルールと、Reactのインデックス管理のルールがねじれてしまうため、JavaScriptの構文としてエラーにならなくても、Reactのランタイムが破綻してしまうのです。
—
現場で役立つ!安全な回避策(プラクティス)
「じゃあ、どうしてもループ内で動的にデータを扱いたいときはどうすればいいの?」と思いますよね。
答えはシンプルです。「フック自体はコンポーネントのトップレベル(一番外側のスコープ)で常に同じ順番・回数で呼び出し、ループや条件分岐は『データ(配列やオブジェクト)』に対して行う」ようにします。
👍 正しい実装例
import React, { useState } from ‘react’;
function GoodTodoList() {
// ✅ フックは常にトップレベルで、順番を崩さずに呼び出す
const [todos, setTodos] = useState([
{ id: 1, text: ‘JavaScriptを極める’, completed: false },
{ id: 2, text: ‘Reactの仕組みを理解する’, completed: true },
]);
return (
-
{/ データの側をループ(map)させて要素を展開する /}
- {todo.text}
{todos.map((todo) => (
))}
);
}
このように、フック自体の呼び出し回数と順番は常に一定(予測可能)に保ち、画面に描画する要素や中身のデータ構造(`todos` の中身など)をループさせるのが、Reactにおける黄金律です。
—
まとめ
いかがでしたでしょうか? 今回のポイントを最後にギュッとまとめますね。
1. Reactのフックは「名前」ではなく「呼び出し順序(インデックス)」でメモリ上の状態を管理している。
2. ループや条件分岐の中でフックを呼ぶと、再描画時にフックの実行順序が狂い、Reactの内部状態が完全に破壊される。
3. JavaScriptのブロックスコープとは無関係に、React側のメモリ管理ルールが優先されるため、構文エラーにならなくてもバグる。
4. 対策として、フックは常にトップレベルで一定回数呼び出し、動的な処理は「データ」の側(`map` など)で行うこと。
ここをしっかり押さえておけば、もうReactの「Hooksのルール(Rules of Hooks)」で迷うことはありません。一見すると制約に思えるこのルールも、JavaScriptのランタイムとReactの仕組みを知れば、理にかなった美しい設計であることが見えてきますよね。
ここをクリアしたあなたなら、どんな複雑なコンポーネント設計も自信を持って乗り越えられるはずです。引き続き、一緒に楽しくJavaScriptを極めていきましょう!