何が起きたのか
Reactアプリが正常に動いていたのに、突然白い画面になった。コンソールを開くと、こんなエラーが表示される:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
これは警告ではなく、ハードクラッシュだ。修正するまでReactは何もレンダリングしない。
このエラーの原因は3つある。順番に確認していこう——最初の原因でつまずく開発者が最も多い。
原因1:プロジェクト内にReactが2つ存在する
特にMaterial UI、Ant Designのようなライブラリや、自分で開発中のローカルパッケージをインストールした後によく起きる、意外と多い原因だ。
確認方法
プロジェクトのルートで以下を実行する:
npm ls react
pnpm/yarnの場合:
pnpm why react
yarn why react
Reactが2つの異なるパスに表示されているなら、それが原因だ。アプリは一方のコピーを使い、ライブラリはもう一方を使う。ライブラリがフックを呼び出すとき、Reactは共有状態のコンテキストを見つけられずクラッシュする。
修正方法
重複が自分で開発中のローカルパッケージ(npm linkまたはモノレポのワークスペースでシンボリックリンクされている)から発生している場合、ライブラリをアプリのReactに向けるようにする。ライブラリのwebpackコンフィグで:
// webpack.config.js (ライブラリ側)
const path = require('path');
module.exports = {
// ...
resolve: {
alias: {
react: path.resolve('./node_modules/react'),
},
},
};
Viteの場合:
// vite.config.js (ライブラリ側)
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
react: path.resolve('./node_modules/react'),
},
},
});
重複がサードパーティパッケージから来ている場合、ピア依存関係のバージョン不一致が原因であることが多い。Reactを更新してクリーンインストールを行う:
npm install react@latest react-dom@latest
rm -rf node_modules package-lock.json
npm install
原因2:関数コンポーネントの外でフックを呼び出している
useState、useEffect、useRef——useで始まる関数はすべて——関数コンポーネントの本体内か、カスタムフックの中で直接呼び出す必要がある。それだけだ。それ以外の場所で呼び出すとReactはこのエラーをスローする。
よくある間違い
通常の関数の中でフックを使う:
// NG — コンポーネントではなく通常の関数
function fetchUserData(userId) {
const [data, setData] = useState(null); // エラー!
// ...
}
// OK — フックをコンポーネント内に移動する
function UserProfile({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setData);
}, [userId]);
return {data?.name};
}
イベントハンドラーの中でフックを使う:
// NG
function LoginButton() {
function handleClick() {
const [token, setToken] = useState(''); // エラー!
}
return Login;
}
// OK — stateはコンポーネントレベルに置く
function LoginButton() {
const [token, setToken] = useState('');
function handleClick() {
setToken('abc123');
}
return Login;
}
条件分岐の中でフックを使う:
// NG — フックをif/elseの中に置くことはできない
function Dashboard({ isLoggedIn }) {
if (isLoggedIn) {
const [profile, setProfile] = useState(null); // エラー!
}
}
// OK — 常にフックを呼び出し、値を条件付きで扱う
function Dashboard({ isLoggedIn }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
if (isLoggedIn) {
fetchProfile().then(setProfile);
}
}, [isLoggedIn]);
}
原因3:クラスコンポーネントの中でフックを使っている
フックはクラスコンポーネントでは動作しない——これは絶対だ。マイグレーションの途中でよくある失敗で、クラスを変換する前にuseStateを追加し始めてしまうことがある。
// NG — クラスコンポーネント + フック = エラー
class UserCard extends React.Component {
render() {
const [count, setCount] = useState(0); // エラー!
return {count};
}
}
// OK — 関数コンポーネントに変換する
function UserCard() {
const [count, setCount] = useState(0);
return {count};
}
今すぐクラス全体を変換できない?フックのロジックを小さな子関数コンポーネントに切り出して、クラスの中でレンダリングしよう。スマートな方法ではないが、とりあえず問題を解決できる。
診断チェックリスト
npm ls reactを実行して——複数のバージョンが表示されていないか?- フックは関数コンポーネントのトップレベルにあるか——
if、for、ネストされた関数の中ではないか? - コンポーネントはクラスではなく関数(
function Foo()またはconst Foo = () =>)か? - コンポーネント名は大文字で始まっているか?Reactはこれを使ってコンポーネントと通常の関数を区別する。
- 間違ったコンテキストから別のフックを内部で呼び出しているカスタムフックを使っていないか?
修正を確認する
変更を加えたら:
- ブラウザのコンソールをクリアする。
- 強制リフレッシュする(Windows/Linuxは
Ctrl+Shift+R、MacはCmd+Shift+R)。 - Invalid hook callエラーが消えているはずだ。
- コンポーネントがクラッシュなしにレンダリングされる。
ブラウザに届く前に検出したい?ESLintプラグインをインストールしよう:
npm install --save-dev eslint-plugin-react-hooks
ESLintの設定に追加する:
// .eslintrc.json
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
このプラグインはコードを書きながら問題のあるフック呼び出しをフラグで示してくれる——実際にアプリを起動して確認する必要はない。
まとめ
- ライブラリをインストールした直後にエラーが出た?まずReactの重複コピーを確認しよう。最も見つけにくく、最もよくある原因だ。
- フックは関数コンポーネントまたはカスタムフックのトップレベルに置く——条件分岐、ループ、ネストされた関数の中には置かない。
eslint-plugin-react-hooksはlint時に3つの原因すべてを検出する。一度設定すれば、このエラーのデバッグに二度と時間を使わずに済む。

