【入門編】Reactのフック(Hooks)とスコープの罠:なぜループ内で宣言してはいけないのか – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!日々のフロントエンド開発、本当にお疲れ様です。
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)させて要素を展開する /}
    {todos.map((todo) => (

  • {todo.text}
  • ))}

);
}

このように、フック自体の呼び出し回数と順番は常に一定(予測可能)に保ち、画面に描画する要素や中身のデータ構造(`todos` の中身など)をループさせるのが、Reactにおける黄金律です。

—

まとめ

いかがでしたでしょうか? 今回のポイントを最後にギュッとまとめますね。

1. Reactのフックは「名前」ではなく「呼び出し順序(インデックス)」でメモリ上の状態を管理している。
2. ループや条件分岐の中でフックを呼ぶと、再描画時にフックの実行順序が狂い、Reactの内部状態が完全に破壊される。
3. JavaScriptのブロックスコープとは無関係に、React側のメモリ管理ルールが優先されるため、構文エラーにならなくてもバグる。
4. 対策として、フックは常にトップレベルで一定回数呼び出し、動的な処理は「データ」の側(`map` など)で行うこと。

ここをしっかり押さえておけば、もうReactの「Hooksのルール(Rules of Hooks)」で迷うことはありません。一見すると制約に思えるこのルールも、JavaScriptのランタイムとReactの仕組みを知れば、理にかなった美しい設計であることが見えてきますよね。

ここをクリアしたあなたなら、どんな複雑なコンポーネント設計も自信を持って乗り越えられるはずです。引き続き、一緒に楽しくJavaScriptを極めていきましょう!

タイトルとURLをコピーしました