できない.dev

React で「Rendered more hooks than during the previous render」が解決できない

フックを条件分岐や早期 return の後で呼ぶと、レンダーごとに呼ぶ数が変わって出る。
フックは必ずコンポーネント先頭で無条件に呼ぶ。

公開: 更新:

症状できない · exit 1
node bad.js
…
Warning: React has detected a change in the order of Hooks called by Panel. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
   Previous render            Next render
   ------------------------------------------------------
解決後できた · exit 0
node solve_hooks_first.js
1 回目(data なし): null
2 回目(data あり): {"type":"div","props":{},"children":["panel:closed"]}

2026-09-20 に node:20(Node.js 20.20.2 / npm 10.8.2) で実際に打って取った出力。検証環境の詳細

要約

Rendered more hooks than during the previous render. は、あるレンダーでは呼ばれなかったフックが次のレンダーで呼ばれ、フックの呼び出し数がレンダー間で食い違ったときに出る。

React はフックを「毎回同じ順序・同じ数」で呼ぶ前提で状態を管理している。
条件分岐や早期 return の後にフックを置くと、この前提が崩れて壊れる。

実行例

node:20 で試すと、1 回目は早期 return でフックが 1 個しか呼ばれず、2 回目に 2 個目へ進んだところでフックの順序が変わった旨の警告が出たうえでエラーになった。
フックを早期 return より前に移した版では、同じ 2 回のレンダーが通っている。

$ node bad.js
1 回目(data なし・フック 1 個): null
Warning: React has detected a change in the order of Hooks called by Panel. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
 
   Previous render            Next render
   ------------------------------------------------------
1. useState                   useState
2. undefined                  useState
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
    at Panel (/tmp/tmp.SwDOABtQe7/bad.js:5:18)
The above error occurred in the <Panel> component:
 
    at Panel (/tmp/tmp.SwDOABtQe7/bad.js:5:18)
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
/tmp/tmp.SwDOABtQe7/node_modules/react/cjs/react.development.js:2526
      throw error;
      ^
 
Error: Rendered more hooks than during the previous render.
    at updateWorkInProgressHook (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5798:13)
    at updateReducer (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5859:14)
    at updateState (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:6278:10)
    at Object.useState (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7146:16)
    at Object.useState (/tmp/tmp.SwDOABtQe7/node_modules/react/cjs/react.development.js:1622:21)
    at Panel (/tmp/tmp.SwDOABtQe7/bad.js:8:24)
    at renderWithHooks (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5608:18)
    at updateFunctionComponent (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:9512:20)
    at beginWork (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:11365:16)
    at performUnitOfWork (/tmp/tmp.SwDOABtQe7/node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15850:12)
 
Node.js v20.20.2
$ echo $?
1
$ node solve_hooks_first.js
1 回目(data なし): null
2 回目(data あり): {"type":"div","props":{},"children":["panel:closed"]}
$ echo $?
0

— 2026-09-20 時点の出力

検証環境Node.js 20.20.2 / npm 10.8.2 / Python 3.11.2 / 2026-09-20 検証(ほか 1 件)
検証日
実行環境
node:20 Debian GNU/Linux 12 (bookworm)
バージョン
  • Node.js 20.20.2
  • npm 10.8.2
  • Python 3.11.2
  • Git 2.39.5

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

解決策

1. フックを先頭でまとめて呼ぶ

function Panel({ id }: { id: number }) {
  const [open, setOpen] = useState(false); // 先に全部呼ぶ
  const data = useData(id);
  if (!data) return null; // return はフックの後
  return <div>{open ? data.body : null}</div>;
}

フックはコンポーネント本体の先頭で、条件や return より前に無条件で呼ぶ。

2. 条件はフックの中に入れる

useEffect(() => {
  if (!open) return; // 呼び出しは常に、中身だけ条件分岐
  subscribe(id);
}, [open, id]);

「条件付きで副作用を走らせたい」場合は、フック自体は毎回呼び、内側で分岐する。

3. リストは子コンポーネントに切り出す

{items.map((item) => (
  <Row key={item.id} item={item} /> // 各 Row の中でフックを呼ぶ
))}

要素ごとにフックが要るなら、要素を別コンポーネントにして、その中で先頭フックとして呼ぶ。

よくある原因

  1. 早期 return の後のフック: if (!data) return null; の下に useState や useEffect がある。
  2. 条件付き呼び出し: if (open) { useEffect(...) } のように、フックを分岐の中で呼んでいる。
  3. ループ内フック: 配列の length 次第で、呼ぶフックの数が変わる。

この手順で直った?