Menu

React useEffectの使い方:依存配列、クリーンアップ、例

useEffectはReactが画面を更新した後にコードを実行するので、コンポーネントはReactの外にあるもの(タイマー、購読、イベントリスナー)と同期できます。依存配列の3つの形、クリーンアップ、無限ループ、そしてそもそもエフェクトが不要な場面を学びます。

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

useEffect は、Reactがコンポーネントをレンダリングして画面を更新した後に、コードを実行します。タイマー、ネットワーク接続、イベントリスナーのような、Reactの外にあるものとコンポーネントを同期させるために使い、いつ再実行するかは依存配列で制御します。

プレビューの下のコンソールを開いてください。エフェクトはコンポーネントが現れたときに1回、その後クリックするたびにログを出します。[count] を [] に変えてもう一度クリックすると、最初のログの後、エフェクトは何も出さなくなります。

構文

useEffect(() => {
    // setup: runs after the render is on the screen
    return () => {
        // cleanup: runs before the next setup, and on unmount
    };
}, [dependencies]);
  • 第1引数はセットアップ関数です。クリーンアップ関数を返すことも、何も返さないこともできます。
  • 第2引数は依存配列です。セットアップが読むコンポーネント内のすべての値(props、state、そしてそれらから計算したもの)を入れます。
  • ほかのすべてのフックと同じく、useEffect はコンポーネントのトップレベルで呼びます。

エフェクトはレンダリングの後に実行される

レンダリングとは、画面に何を表示すべきかを知るためにReactがコンポーネント関数を呼ぶことです。エフェクトはその作業が終わってDOMが更新されるまで待つので、エフェクトの中ではページにすでに新しい値が表示されています。

クリックするたびに、まずレンダリングが、次にエフェクトがログを出し、エフェクトはReactが今書いたボタンのテキストを読みます。DOMの読み取り、タイマーの開始、サーバーへの接続をコンポーネント本体ではなくエフェクトに書くのはこのためです。本体はレンダリング中、つまり画面に何も表示される前に実行され、Reactは思ったより頻繁にそれを呼ぶことがあります。

エフェクトは通常、ブラウザが画面を描画した後に実行されます。描画の前にDOMを測る必要があるエフェクトには、常に描画の前に実行されるuseLayoutEffectを使ってください。

依存配列:3つの形

依存配列は、エフェクトをいつ再実行するかを決めます。形は3つあり、次のブロックはそのすべてを使っているので、違いを確かめられます。

読み込み時には3つのエフェクトすべてがログを出します。a + 1 をクリックすると、配列のないものと a に依存するものの2つが実行されます。b + 1 をクリックすると、配列のないものだけが実行されます。[] にも [a] にも変わったものがないからです。

依存配列エフェクトが実行されるとき
なしレンダリングのたびに
[]最初のレンダリングの後に1回(マウント時)
[a, b]最初のレンダリングの後と、その後 a か b が変わるたびに

Reactは各依存値を前回の値と Object.is で比べます。数値、文字列、真偽値は値で比べられます。オブジェクト、配列、関数は同一性で比べられ、これが大きな意味を持ちます(後述)。

エフェクトの実行回数を減らすために値を配列から外してはいけません。エフェクトは古いレンダリングの値を使い続けることになります。react-hooks/exhaustive-deps のlintルールがこれを検出します。ViteのReactテンプレートやNext.jsのESLint設定に含まれているESLintの設定の一部です。

クリーンアップ

多くのエフェクトは、止める必要のあるもの(接続、購読、タイマー)を始めます。エフェクトから関数を返すと、Reactは2つのタイミングでそれを呼びます。新しい依存値でエフェクトを再実行する直前と、コンポーネントが画面から消えるとき(アンマウント)です。

別のルームを選んでください。コンソールには disconnect from general、続いて connect to music と表示されます。新しい接続が開く前に、古い接続は必ず閉じられます。Hideをクリックすると、ChatRoom がアンマウントされたので、クリーンアップが最後にもう1回実行されます。

ここでの connect 関数は、本物のチャットやWebSocketのクライアントの代わりです。どのクライアントでも形は同じで、セットアップで作り、クリーンアップで閉じます。

タイマーを解除する

エフェクトで始めたタイマーは、クリーンアップで止めない限り、コンポーネントが消えた後も動き続けます。

clearInterval(id); の行を削除してからタイマーを隠してみてください。もう存在しないコンポーネントから、コンソールに tick が届き続けます。Showを押すたびに、さらにもう1つ始まります。

setSeconds((s) => s + 1) に注目してください。エフェクトは1回しか実行されないので、普通の setSeconds(seconds + 1) だと最初のレンダリングの seconds をずっと読み続け、1で止まってしまいます。更新関数は常に最新の値を受け取ります。

イベントリスナーを削除する

window や document のイベントリスナーも同じパターンです。addEventListener に渡したのと同じ関数を removeEventListener に渡します。

useEffect(() => {
    function handleKeyDown(e) {
        if (e.key === 'Escape') onClose();
    }
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);

無限ループ

自分が依存しているstateの値をセットするエフェクトは、ループを始めてしまいます。stateが変わり、Reactがレンダリングし、依存値が変わったのでエフェクトが実行され、またstateをセットするのです。

// Never stops: every render runs the effect, every effect causes a render
useEffect(() => {
    setCount(count + 1);
});

// Same loop: count changes, so the effect runs again
useEffect(() => {
    setCount(count + 1);
}, [count]);

コンポーネントは終わりなくレンダリングを続け、タブは忙しいままになります。Reactはこのループを止めません。開発中はコンソールに「Maximum update depth exceeded」エラーを記録し、本番では何も記録しません。次のブロックは2つ目の版を停止条件付きで実行するので、ページを固まらせずにループを観察できます。

コンソールには render 0 から render 5 までが記録されます。1回のマウントで6回のレンダリングが起き、それぞれをエフェクトが始めています。直し方は、何をしたかったかによります。

  • 1回だけ実行したかった。[] と更新関数の形 setCount((c) => c + 1) を使い、count を依存値にしないようにします。
  • ほかのstateから値を計算している。そもそも保存しないでください。このページの最後のセクションで示すように、レンダリング中に計算します。
  • ユーザーの操作に反応している。そのコードはイベントハンドラに書きます。

依存値としてのオブジェクトと関数

コンポーネント本体で作ったオブジェクト、配列、関数はレンダリングのたびに新しい値になるので、それに依存するエフェクトは、中身が何も変わっていなくてもレンダリングのたびに実行されます。

色の切り替えは検索と何の関係もないのに、切り替えるたびに search for react がまた記録されます。options の行をエフェクトの中に移し、配列を [query] に変えてみてください。切り替えでは何も出なくなり、入力欄に入力したときだけ検索が実行されます。

これが一般的な直し方です。オブジェクトや関数はそれを使うエフェクトの中で作り、それらを組み立てるプリミティブな値に依存させます。関数をエフェクトの外に置く必要があるときは、useCallbackがレンダリング間でその同一性を保ちます。

開発中にエフェクトが2回実行される理由

<StrictMode> が有効だと(新しいViteアプリはルートをこれで包み、Next.jsのApp Routerはデフォルトで有効にします)、Reactは開発中にテストを行います。各コンポーネントをマウントし、エフェクトを実行し、そのクリーンアップを実行して、もう一度エフェクトを実行するのです。上のチャットルームなら、開発中のコンソールには次のように表示されます。

connect to general
disconnect from general
connect to general

これはマウント時に1回だけ、開発中だけ起きます。クリーンアップが欠けているエフェクトをあぶり出すためのものです。続けて2回実行すると何かが壊れるなら、ユーザーが画面を離れて戻ってきたときにも壊れるでしょう。直し方は、refで2回目の実行を防ぐことでは決してなく、クリーンアップを書くことです。このページのプレビューは本番ビルドのように動くので、ここでは2回の実行は見えません。

エフェクトは不要かもしれない

エフェクトはReactの外のシステムと同期するためのものです。よくある使い方のうち2つは、エフェクトなしのほうがよくなります。

propsやstateから計算できる値。レンダリング中に計算してください。stateに保存してエフェクトで同期すると余分なレンダリングが1回かかり、そのうち最初のレンダリングではDOMに古い値が入ります。

クリックするたびに WithEffect は2回レンダリングされ、その最初のレンダリングではまだ古い名前が表示されます。Computed は正しい値で1回だけレンダリングされます。

ユーザーの操作に反応するコード。フォームの送信、購入後のトースト表示、クリックに対する分析イベントの送信は、イベントハンドラに書きます。そこでは何が起きたかが正確にわかりますが、エフェクトにわかるのは何かの値が変わったことだけです。

useEffectEvent

エフェクトが、あるpropの最新の値を必要としつつ、そのpropが変わっても再実行したくないことがあります。チャットルームなら、通知には現在の theme を使うべきですが、テーマを切り替えても再接続すべきではありません。React 19.2では、このために useEffectEvent が追加されました。

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
    const onConnected = useEffectEvent(() => {
        showNotification('Connected!', theme); // always the latest theme
    });

    useEffect(() => {
        const connection = createConnection(roomId);
        connection.on('connected', () => onConnected());
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]); // theme is not a dependency
}

useEffectEvent から得た関数は、エフェクトの中で呼ぶためだけのものです。ほかのコンポーネントに渡したり、依存配列に入れたりしないでください。

よくある質問

useEffectはいつ実行されますか?

Reactがコンポーネントをレンダリングし、DOMを更新した後です。通常はブラウザが画面を描画した後になります。依存配列がなければレンダリングのたびに、[] なら最初のレンダリングの後だけ、[a, b] なら最初のレンダリングの後と、a か b が変わったレンダリングの後に実行されます。

useEffectのクリーンアップ関数とは何ですか?

エフェクトから返す関数です。Reactは新しい依存値でエフェクトを再実行する前と、コンポーネントが画面から取り除かれるときにもう一度それを呼びます。タイマーの解除、イベントリスナーの削除、購読の終了に使います。

useEffectが2回実行されるのはなぜですか?

開発中は、クリーンアップが欠けているエフェクトを見つけられるよう、<StrictMode> がすべてのコンポーネントをマウントし、エフェクトを実行し、クリーンアップしてから、もう一度実行します。これはマウント時に1回だけ、開発中だけ起きます。本番ビルドではエフェクトは1回だけ実行されます。

useEffectで無限ループが起きるのはなぜですか?

エフェクトが、自分の依存配列にも入っているstateの値をセットしているからです(または配列がまったくない場合)。stateをセットすると再レンダリングされ、依存値が変わったのでエフェクトがまた実行されます。更新関数を使って依存値から外すか、条件を付けるか、代わりにレンダリング中に値を計算してください。

useEffectのコールバックをasyncにできますか?

いいえ。コールバックは何も返さないか、クリーンアップ関数を返す必要がありますが、async 関数はPromiseを返します。データの取得のページで紹介しているように、エフェクトの中でasync関数を定義して呼んでください。

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

Coddyでコードを学ぼう

始める