Menu

Reactのフックのルール:呼び出し順が重要な理由と直し方

フックには2つのルールがあります。コンポーネントのトップレベルで呼び、条件分岐、ループ、ネストした関数の中では決して呼ばないこと。そして関数コンポーネントかカスタムフックからだけ呼ぶことです。Reactがすべてのレンダリングで同じ呼び出し順を必要とする理由と、それが変わったときに出るエラーの直し方を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

フックのルールは2つです。フックはコンポーネントのトップレベルでだけ呼び、条件分岐、ループ、ネストした関数の中や早期 return の後では決して呼ばないこと。そして関数コンポーネントかカスタムフックからだけ呼ぶことです。これらのルールがあるのは、Reactが呼び出しの順番でフックを区別しているので、その順番がすべてのレンダリングで同じでなければならないからです。

Reactは、コンポーネントのフックの値を、そのコンポーネントに結びついたリストに保存しています。レンダリングのたびにリストをたどり、1つ目のフック呼び出しには1つ目の枠を、2つ目の呼び出しには2つ目の枠を、という具合に渡します。名前はなく、あるのは位置だけです。次の例は、その考え方を普通のJavaScript数行で再現したおもちゃ版で、React自身のコードではないので、安心して壊せます。

最初のレンダリングで3つの枠が埋まります:Ada、Countess、36。「Render without nickname」をクリックすると、条件付きの呼び出しが飛ばされるので、age の呼び出しが枠1を取って Countess を読みます。本物のReactは、場合によってはこの食い違いに気づいて例外を投げ、場合によっては間違った値を間違ったフックに渡します。後者のほうが厄介です。toyUseState('Countess') の呼び出しを if の上に移すと、どちらのボタンでも正しい年齢になります。

コードで見る2つのルール

function Profile({ user }) {
    // Good: always called, always in this order
    const [tab, setTab] = useState('posts');
    const theme = useContext(ThemeContext);
    useEffect(() => {
        document.title = user.name;
    }, [user.name]);

    // Bad: inside a condition, a loop or a nested function
    if (user.isAdmin) {
        const [log, setLog] = useState([]); // breaks the order
    }
}

必ず到達し、同じ順番で呼ばれるフック呼び出しなら、早期 return より前にある限り、本体のどこにあっても問題ありません。

条件付きで呼ばれたフックを直す

linterはこれを React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. と報告します。直し方はいつも同じです。フックは毎回呼び、条件をその中に入れます。

// Broken: the effect exists on some renders and not others
function Notifications({ user }) {
    if (user) {
        useEffect(() => {
            console.log('subscribe for', user.name);
        }, [user]);
    }
    return null;
}

エフェクトはすべてのレンダリングで宣言され、中の if が何かをするかどうかを決めます。何回かログインとログアウトを繰り返すと、コンソールにはログインごとにsubscribeが、ログアウトごとにunsubscribeが表示されます。

早期returnの後のフック

早期 return も条件の一種です。次のコードは、loading が初めて false になったときに「Rendered more hooks than during the previous render」を起こします。そのレンダリングが、前回のレンダリングでは一度も呼ばれなかったフックに到達するからです。前回より少ないフックしか呼ばないレンダリングでは、メッセージは「Rendered fewer hooks than expected」になります。

// Broken: useState is skipped while loading
function Comments({ loading, comments }) {
    if (loading) return <p>Loading...</p>;
    const [expanded, setExpanded] = useState(false);
    return <List items={comments} expanded={expanded} />;
}

すべてのフックが return より上にあるので、どちらの分岐でも同じフックが呼ばれます。「Toggle loading」をクリックし、リストを展開してから、loadingを2回切り替えてください。stateが一度も飛ばされていないので、リストは展開していたことを覚えています。

ループの中のフック

項目の数は変わるので、ループで項目ごとに useState を1回呼ぶことはできません。各項目に独自のコンポーネントを与えて、そこでフックを呼びます。各インスタンスが自分の枠を持ちます。

「Invalid hook call」

このメッセージは「Invalid hook call. Hooks can only be called inside of the body of a function component.」で始まります。よくある原因は3つです。

  1. Reactが関数コンポーネントをレンダリングしていないときにフックが実行されている。イベントハンドラ、エフェクトのコールバックの中、クラスコンポーネント、モジュールのトップレベルなどです。コンポーネント本体かカスタムフックに移し、カスタムフックはコンポーネントから呼んでください。
  2. react と react-dom のバージョンが一致していない。両方を同じバージョンでインストールしてください。
  3. Reactのコピーが2つある。リンクしたローカルパッケージや、独自のReactを同梱したライブラリが2つ目のコピーを持ち込み、一方のコピーのフックはもう一方のレンダラーでは動きません。npm ls react を実行し、バージョンが1つだけ表示されることを確認してください。
npm ls react

ここのプレビューは本番ビルドのように動き、これらのメッセージは表示されません。イベントハンドラから呼んだフックは「Cannot read properties of null」のような普通の TypeError で失敗し、React自身のエラーは短い「Minified React error」のコードとして届きます。全文を読むには、アプリを開発モードで実行してください。

linterに守らせる

eslint-plugin-react-hooks は入力中に両方のルールを確認し、エフェクトの依存配列も確認します。ViteのReactテンプレートとNext.jsのESLint設定にはこれが含まれています。フラット形式のESLint設定では、推奨のプリセットを有効にします。

// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';

export default defineConfig([reactHooks.configs.flat.recommended]);

中心となる2つのルールは react-hooks/rules-of-hooks(エラー)と react-hooks/exhaustive-deps(警告)です。バージョン7からは、このプリセットはReact Compiler由来のルールも有効にします。react-hooks/purity や react-hooks/refs などで、純粋でないレンダリングやレンダリング中のrefの読み取りを指摘します。ルールを1つずつ選びたいなら、プラグインを plugins: { 'react-hooks': reactHooks } として登録し、使いたいルールを並べてください。

プラグインは名前でフックを見つけます。名前が use と大文字で始まる関数はフックとして扱われ、それを呼べるのはコンポーネント(大文字で始まる名前)とほかの use 関数だけです。カスタムフックがこの接頭辞を使わなければならないのはそのためです。

例外:use

React 19で追加された use はPromiseやコンテキストを読み、ほかのすべてのフックと違って、条件分岐やループの中で呼べます。それでもレンダリング中に、コンポーネントかフックの中で呼ぶ必要があります。

テーマを切り替えると、色が変わるのはthemedの見出しだけです。同じことを if の中の useContext で試すと、linterが指摘します。use でPromiseを読む方法はuseフックのページで扱っています。

よくある質問

フックのルールとは何ですか?

2つのルールです。フックは関数コンポーネントかカスタムフックのトップレベルでだけ呼び、条件分岐、ループ、ネストした関数の中や早期returnの後では決して呼ばないこと。そしてフックは関数コンポーネントかカスタムフックからだけ呼び、普通の関数やクラスコンポーネントからは呼ばないことです。

フックを条件付きで呼べないのはなぜですか?

Reactはフックを名前で区別していないからです。値をリストに保存し、呼び出しが起きた順番で返します。条件によって呼び出しが1つ飛ばされると、それ以降のすべてのフックが、1つ前のフックの値を読んでしまいます。

"Rendered more hooks than during the previous render" はどういう意味ですか?

あるレンダリングが前回のレンダリングより多くのフックを呼んだという意味です。たいていは、フックが if の中や早期 return の後にあり、今回は違う経路をたどったのが原因です。すべてのフックを条件より上に移し、代わりに条件をフックの中に入れてください。

"Invalid hook call" はどう直しますか?

3つのことを確認してください。フックがReactの関数コンポーネントのレンダリング中に実行されていること(イベントハンドラ、エフェクトのコールバック、クラスコンポーネント、モジュールのトップレベルではないこと)、react と react-dom のバージョンが一致していること、そしてアプリにReactのコピーが1つしかないこと(npm ls react で一覧できます)です。

useフックは条件付きで呼べますか?

はい。use(React 19)は唯一の例外で、if ブロックやループの中で呼べます。それでもコンポーネントかフックの中で呼ぶ必要があり、イベントハンドラの中では呼べません。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める