エラーの状況
アプリは問題なく動いていました。コンポーネントの先頭に if (!userId) return という防御的なコードを追加したところ、Reactが以下のエラーを投げるようになりました:
Error: Rendered more hooks than during the previous render.
アプリ全体がクラッシュします。2分前まで正常にレンダリングされていたコンポーネントが、今では壊れています。
このエラーを引き起こす典型的なパターンは次の通りです:
function UserProfile({ userId }) {
if (!userId) {
return <p>No user selected.</p>;
}
// ❌ 早期returnの後にHookが呼ばれている
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user?.name}</div>;
}
最初のレンダリング時:userId がセットされており、Reactは両方のHookを実行します。2回目のレンダリング時:userId が undefined になると早期returnが発火し、Reactは2つのHookを期待していた場所でゼロを検出します。このズレがエラーの原因です。
ReactがHookの順序を重視する理由
ReactはHookを名前ではなく位置で追跡します。レンダリングのたびに、固定サイズの内部リストを順番に処理します — スロット0が useState、スロット1が useEffect、といった具合です。条件付きのreturnの後にHookを置くと、レンダリング間でリストの長さが変わります。Reactはどのstateがどのhookに属するかを判断できなくなります。
これがHookのルールです:コンポーネントのトップレベルで、条件なく、すべてのレンダリングで必ずHookを呼び出してください。例外はありません。
クイックフィックス:すべてのHookをreturnより前に移動する
すべてのHookを関数の先頭に巻き上げてください。条件付きのreturnはその後に置きます:
function UserProfile({ userId }) {
// ✅ Hookは常に最初に実行される
const [user, setUser] = useState(null);
useEffect(() => {
if (!userId) return; // ガード処理はエフェクトの内部に移動
fetchUser(userId).then(setUser);
}, [userId]);
// 早期returnはHookの後に置く
if (!userId) {
return <p>No user selected.</p>;
}
return <div>{user?.name}</div>;
}
ガードのロジックは useEffect のコールバック内に移動します。Hook自体は毎回のレンダリングで実行されるため、Reactは正常に動作します。
このルールを破る他の3つのパターン
パターン1:if文の中にHookがある
// ❌ 間違い
function Toggle({ isLoggedIn }) {
if (isLoggedIn) {
const [count, setCount] = useState(0); // ログアウト時にスキップされる
}
return <div />;
}
// ✅ 正しい
function Toggle({ isLoggedIn }) {
const [count, setCount] = useState(0); // 常に実行される
return <div>{isLoggedIn ? count : null}</div>;
}
パターン2:ループの中にHookがある
ループの長さはレンダリング間で変わる可能性があります — 同じ問題が別の形で現れます。
// ❌ 間違い
function ItemList({ items }) {
return items.map((item) => {
const [selected, setSelected] = useState(false);
return <Item key={item.id} selected={selected} />;
});
}
// ✅ 正しい — 子コンポーネントに切り出す
function ItemList({ items }) {
return items.map((item) => <Item key={item.id} />);
}
function Item({ id }) {
const [selected, setSelected] = useState(false); // 独自コンポーネントのトップレベル
return <div onClick={() => setSelected(!selected)}>{id}</div>;
}
パターン3:条件付きカスタムHook
// ❌ 間違い
function DataFetcher({ shouldFetch, url }) {
if (shouldFetch) {
const data = useFetch(url); // 場合によってはスキップされる
}
}
// ✅ 正しい — 条件をHookの中に渡す
function DataFetcher({ shouldFetch, url }) {
const data = useFetch(shouldFetch ? url : null);
}
カスタムHookは、呼び出しを条件でラップするのではなく、null や無効化された値を受け入れるように設計してください。よく設計されたHookライブラリ(SWR、React Query)は、すでにこのパターンをサポートしています。
根本から防ぐ:Hooks用ESLintプラグイン
公式プラグインをインストールすれば、アプリを実行する前にエディタ上でこの問題を検出できます:
npm install eslint-plugin-react-hooks --save-dev
ESLint設定に組み込みます:
// .eslintrc.js
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error', // 条件付きHookを検出
'react-hooks/exhaustive-deps': 'warn', // 依存関係の欠落を検出
},
};
Create React AppとViteのReactテンプレートにはすでにこのプラグインが含まれています。rules-of-hooks が 'warn' ではなく 'error' に設定されていることを確認してください — 警告は締め切りのプレッシャーの下では無視されがちです。'error' にすることで、リンターは違反を見逃さなくなります。
修正が機能しているか確認する
- エラーが消えた — ブラウザのコンソールにクラッシュが表示されなくなります。
- ESLintがクリーン —
npx eslint src/YourComponent.jsxを実行し、react-hooks/rules-of-hooksの違反がゼロであることを確認します。 - エッジケースのテスト — 早期returnを引き起こした条件を手動でトリガーし(例えば
userId={undefined}を渡す)、エラーなくフォールバックUIがレンダリングされることを確認します。 - React DevTools — コンポーネントを開き、propsを数回切り替えます。HookのstateはクラッシュなしにクリーンにUpdateされるはずです。
このエラーを見たときのチェックリスト
- Hook呼び出しより上に現れる
return文がないかスキャンする。 if、switch、三項演算子、またはArray.map()の中にHookがないか確認する。- 条件付きカスタムHookを、無効化/null入力を受け入れるようにリファクタリングする。
- 独自のstateが必要なリストアイテムを、別の子コンポーネントに切り出す。
eslint-plugin-react-hooksを有効にして、実行時ではなく記述時に問題を検出する。

