ReactのInvalid Hook Callエラーを修正する方法
Reactのinvalid hook callエラーを、スタックトレース、Rules of Hooks、Reactの重複コピー、react-domの不一致で見分けて修正します。
invalid hook callエラーの一般的な原因は3つあります。自分のコードでのRules of Hooks違反、アプリ内にReactのコピーが複数存在すること、そしてreactとreact-domのバージョン不一致です。どれを最初に確認すべきかは、スタックトレースが教えてくれます。
このエラーは、自分のコンポーネントコードには問題がない場合にもしばしば発生します。ローカルのコンポーネントライブラリをnpm linkしたとき、依存関係を追加したとき、monorepoを再構成したときなどに、3つの原因のうちどれなのかを示すことなくエラーが現れるのです。
要点
- 失敗しているhook呼び出しが自分のコンポーネントファイル内にあるなら、問題はhookの呼び出し場所にあります。
node_modules内にあるなら、ほぼ確実にReactの2つ目のコピーが原因です。 npm ls react(またはpnpm why react、yarn why react)を実行してください。出力で複数バージョンのReactが解決されている場合、原因は重複コピーであり、コンポーネントコードをいくら変更しても解決しません。- コンポーネントライブラリはReactを
peerDependenciesに宣言し、ビルド出力から除外しなければなりません。自前のReactをバンドルしてしまうと、それを利用するすべてのアプリでコピーが2つになります。 rules-of-hookslintルールは、コードが実行される前に不適切な位置のhook呼び出しを検出しますが、Reactの重複コピーはどのlinterでも検出できません。この失敗はソースではなくインストールされた依存関係ツリーに存在するからです。- 本番環境ではこのエラーはminified error #321として現れます。原因を推測する前に、Reactのerror decoderでデコードしてください。
Invalid Hook Callエラーとは何を意味するのか
Reactは、hookが関数コンポーネントのレンダー外で実行されるたびにこのエラーをスローします。メッセージ自体が可能性を列挙しています。
Invalid hook call. Hooks can only be called inside of the body of a function component.
This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
Reactのinvalid hook call警告ページでは、この3つすべてと、より稀なケースを扱う包括的なセクションが説明されています。本記事の以降では、このエラーが通常どのように現れるかに合わせた順序で確認していきます。
まずスタックトレースを読む
設定に手をつける前に、スタックトレースから1つの問いに答えてください。hookを呼び出しているフレームは、自分のソースファイル内にあるのか、それともnode_modules内にあるのか。自分のコンポーネントファイルを指しているなら、Rules of Hooks違反であり、修正すべきは自分のコードです。以前は問題なく動作していた依存関係の中を指しているなら、ほぼ確実にReactのコピーが2つあり、コンポーネントを編集しても結果は変わりません。
| 原因 | 確認方法 | 修正 |
|---|---|---|
| Rules of Hooks違反 | スタックトレースが自分のファイルを指している | hookをコンポーネントのトップレベルに移動する |
| Reactのコピーが2つ | npm ls reactが2つのバージョンを解決する | 依存ツリーをdedupeする(下記参照) |
react/react-domの不一致 | npm ls react react-domが異なるバージョンを表示する | 両方を一緒にインストールする |
自分のコードにあるReact Invalid Hook Callを修正する
このエラーを引き起こすルールは2つだけです。hookは関数コンポーネントのレンダー中(またはコンポーネントが呼び出すカスタムhookから)呼び出されなければならず、かつそのコンポーネントのトップレベルに置かれなければならない、つまりifやループ、ネストされた関数の中には置けないということです。モジュールレベル、イベントハンドラ内、あるいは普通のヘルパー関数内のhookは1つ目のルールを破ります。条件分岐や.mapコールバック内のhookは2つ目のルールを破ります。
ヘルパー関数のケースは、コードが一見妥当に見えるため意外に思われがちです。
// Wrong: buildLink is a plain function, not a component
export function buildLink() {
const { pathname } = useLocation(); // invalid hook call
return `https://example.com${pathname}`;
}
// Right: call the hook in a component, pass the value down
function Page() {
const { pathname } = useLocation();
return <a href={buildLink(pathname)}>Canonical</a>;
}
export function buildLink(pathname) {
return `https://example.com${pathname}`;
}
ループのケースでは、修正は構造的なものになります。子コンポーネントを抽出し、各アイテムが自身のstateを持つようにします。
// Wrong: one hook call per array item
function List({ items }) {
return items.map((item) => {
const [open, setOpen] = useState(false); // invalid hook call
return <li key={item.id}>{item.name}</li>;
});
}
// Right: each row is a component with its own state
function Row({ item }) {
const [open, setOpen] = useState(false);
return <li onClick={() => setOpen(!open)}>{item.name}</li>;
}
function List({ items }) {
return items.map((item) => <Row key={item.id} item={item} />);
}
なぜReactのコピーが2つあるとhookが壊れるのか
hookは、アプリとreact-domが同じreactモジュールをロードしている場合にのみ動作します。それぞれが独自のコピーを取得してしまうと、コード内のすべてのhook呼び出しが正しい位置にあっても、Reactはこのエラーをスローします。他のことに手をつける前に確認してください。
npm ls react # npm
pnpm why react # pnpm
yarn why react # yarn
pnpm whyとyarn whyは、パッケージからそれを引き込んだ元へと逆向きに辿るので、どの依存関係が2つ目のコピーを引き込んでいるのかを正確に確認できます。重複のほとんどは次の2つの状況によるものです。
リンクされたローカルパッケージ。 npm linkやpnpm linkでリンクされたライブラリは、あなたのnode_modulesではなく自身のnode_modulesからReactを解決します。だからこそ、通常のインストールでは問題なく動いていたコンポーネントライブラリをリンクした瞬間にこのエラーが現れることが多いのです。Reactのドキュメントではnpm linkのケースが扱われており、修正方法はライブラリがアプリ側に既にインストールされているReactを参照するようにすることです。Viteプロジェクトでは、resolve.dedupeにパッケージを列挙すると、Viteはそれぞれをプロジェクトルートから取得した単一のコピーに固定します。
// vite.config.js
export default {
resolve: { dedupe: ['react', 'react-dom'] },
}
Reactを同梱するライブラリ。 パッケージがreactを通常の依存関係として宣言していたり、ビルド出力にバンドルしていたりすると、すべての利用側がコピーを2つ持つことになります。ライブラリ側の修正は、Reactをサポートするバージョン範囲とともにpeerDependenciesに宣言し、ビルドでexternalとしてマークすることです。アプリ側の回避策は解決先を1つに強制するもので、フィールド名はパッケージマネージャによって異なります。npmはoverridesを読み、$reactは「自分自身がreactに対して宣言しているのと同じバージョン」を意味します。
{ "overrides": { "react": "$react", "react-dom": "$react-dom" } }
yarnは代わりにresolutionsを読み、値には素のバージョンを指定します。1つのファイルに両方のフィールドを書かないでください。各マネージャは他方のキーを無視します。
reactとreact-domの不一致
reactとreact-domはペアで提供されるので、両方を確認し、1つのコマンドでインストールしてください。npm ls react react-domを実行し、2つのバージョンが異なっていれば、同じリリースに解決されるように一緒に再インストールします(npm install react react-dom)。これは最も手早く除外できる原因であり、早めに除外しておくことで、実在しないコードのバグを追いかける事態を避けられます。
Linterでより早く検出するには
eslint-plugin-react-hooksパッケージは、このエラーのコードに起因するすべての原因を編集時に指摘します。ESLintのflat configでは次のようになります。
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([reactHooks.configs.flat.recommended]);
ESLint 9.0.0未満のバージョンでは、レガシー形式は"extends": ["plugin:react-hooks/recommended"]です。Next.jsプロジェクトでは、eslint-config-nextを通じて既にこれらのルールが適用されています。rules-of-hooksルールは、コードが実行される前に条件分岐内や不適切な位置のhook呼び出しを検出しますが、Reactの重複コピーやバージョン不一致はどのlinterでも検出できません。これらの失敗はインストールされた依存関係ツリーにのみ存在するため、実行時にしか表面化しないのです。
本番環境での形態: Minified Error #321
本番ビルドでは、このエラーは完全なメッセージではなくminifiedされたエラーコードとして現れるので、どの問題なのかを決めつける前にコードをデコードしてください。React error #321はinvalid hook callのテキストに展開されます。まずそれを確認することで、間違ったinvariantをデバッグせずに済みます。minifiedされたスタックはスローしたコンポーネント名を示すことがほとんどないため、重複コピーのケースは本番環境で特に追跡が困難です。OpenReplayのようなセッションリプレイツールは、コンソールエラーをルートやその直前のインタラクションとともにキャプチャするため、スローされた瞬間にどのコンポーネントツリーがマウント中だったかを示してくれます。そしてそれは通常、2つ目のReactコピーを持ち込んだlazy-loadedチャンクやサードパーティウィジェットを指し示します。
スタックトレースから始める
このエラーは謎ではなく、経路選択の問題として扱ってください。スタックトレースは、自分のコンポーネント内(hookの配置を修正)か、依存関係ツリー内(npm ls reactを実行してdedupe)のいずれかへとあなたを導きます。まずはあの1つのコマンドから始めましょう。3つの原因のうち最も紛らわしいものを数秒で決着させてくれますし、その後はすべて既知の修正方法があります。
FAQ
invalid hook callエラーを避けるには、カスタムhookは 'use' で始まる必要がありますか?
いいえ。'use' プレフィックスがこのランタイムエラーを引き起こしたり防いだりすることはありません。Reactは実行時にhook名をチェックしないからです。プレフィックスが重要なのはツーリングのためです。eslint-plugin-react-hooksはこれを頼りにhookを認識し、Rules of Hooksを強制するため、誤った名前のカスタムhookはlintチェックを黙って通り抜けてしまいます。ブラウザではなく編集時に違反が指摘されるように、プレフィックスを付けてリネームしてください。
クラスコンポーネント内でhookを呼び出せますか?
いいえ。hookは関数コンポーネントと、そこから呼び出されるカスタムhookでのみ動作するため、クラスメソッド内でuseStateやuseContextを呼び出すとinvalid hook callエラーがスローされます。書き換えられないクラスとともにhookを使うには、hookを呼び出して結果をpropsとしてクラスコンポーネントに渡す小さな関数コンポーネントを作るか、クラスを関数コンポーネントに変換してください。
同一ページ上でReactのコピーが2つ動作していてもエラーにならないことはありますか?
はい。1つのページ上の2つのアプリが、それぞれ独自のReactをロードして問題なく動作することはあります。たとえば異なるチームがそれぞれ別々に出荷している場合です。エラーが発生するのは、コンポーネントと、それをレンダリングしているreact-domインスタンスが、どのreactモジュールを使っているかで食い違っている場合のみです。別々のコピー自体は問題ありません。単一のレンダーツリーを共有した途端に壊れるのです。
node_modulesを削除して再インストールすればReactの重複コピーは解消されますか?
重複が古い、あるいは競合したインストール状態から生じていた場合に限ります。クリーンインストールによってパッケージマネージャが依存ツリーをdedupeできるからです。依存関係がreactを通常の依存関係として宣言している場合、Reactをビルド出力にバンドルしている場合、あるいはnpm linkでローカルにリンクした場合は、インストールごとに2つ目のコピーが戻ってきます。これらのケースには、overridesまたはresolutionsのエントリ、ライブラリ側でのpeerDependenciesの修正、あるいはバンドラレベルのdedupeが必要です。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k