Reactで「Rendered more hooks than during the previous render」を修正する

beginner⚛️ React2026-05-11| React 16.8以降、任意のOS、Create React App・Vite・Next.jsに対応

Error Message

Error: Rendered more hooks than during the previous render.
#react#hooks#rules-of-hooks#conditional-rendering

エラーの状況

アプリは問題なく動いていました。コンポーネントの先頭に 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回目のレンダリング時:userIdundefined になると早期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 文がないかスキャンする。
  • ifswitch、三項演算子、または Array.map() の中にHookがないか確認する。
  • 条件付きカスタムHookを、無効化/null入力を受け入れるようにリファクタリングする。
  • 独自のstateが必要なリストアイテムを、別の子コンポーネントに切り出す。
  • eslint-plugin-react-hooks を有効にして、実行時ではなく記述時に問題を検出する。

Related Error Notes