React

React useEffectを初回だけ実行する方法|Strict Modeで2回動く理由も解説

ReactのuseEffectを初回マウント時だけ実行する方法を解説します。空の依存配列、Strict Modeで開発時に2回動く理由、cleanup、setInterval・fetchの正しい例、依存配列と無限ループ、useRefで抑止してはいけない理由まで整理します。

この記事の目次
  1. useEffectの基本形
  2. 空の依存配列[]で初回mount時に実行する
  3. 1回だけの意味は1mountにつき1回
  4. 依存配列なし・空配列・値ありの違い
  5. Strict ModeでuseEffectが2回動く理由
  6. Strict Modeを外す前にcleanupを直す
  7. setIntervalを初回mount時に開始する正しい例
  8. cleanupがないと何が起きるか
  9. event listenerを1回登録してcleanupする
  10. fetchをuseEffectで実行する場合
  11. dependencyはEffectが読む値に合わせる
  12. component外へ定数を移す
  13. Effect内へobjectや関数を移す
  14. 無限loopが起きる条件
  15. useRefでStrict Modeの2回実行を抑止しない
  16. useEffectを使わないほうがよい処理
  17. props・stateから表示値を計算する
  18. 利用者の操作に反応する
  19. useStateの初期値を重く計算する
  20. application起動時に初期化する
  21. Effect Eventで最新値を読む
  22. useEffectとuseLayoutEffectの選び方
  23. 繰り返す同期処理はCustom Hookへまとめる
  24. 初回だけ実行したいときの判断表
  25. よくある質問
  26. 空配列[]なら絶対に1回だけですか?
  27. Strict Modeで2回動くのを止めるには?
  28. dependency警告を消すため[]へ変えてよいですか?
  29. fetchは初回だけなら[]でよいですか?
  30. useRefでhasRunを管理するのは常に間違いですか?
  31. useEffectはserver renderingで動きますか?
  32. まとめ

ReactのuseEffectをcomponentの初回mount時に実行したい場合は、第二引数へ空の依存配列[]を指定します。

import { useEffect } from "react";

function Sample() {
  useEffect(() => {
    console.log("mount後にsetup");

    return () => {
      console.log("unmount前にcleanup");
    };
  }, []);

  return <p>Sample</p>;
}

ただし、正確には「application全体で永久に1回」ではなく、1回のmountに対する同期処理です。componentがunmountして再びmountすれば、Effectも再度setupされます。またStrict Modeが有効な開発環境では、問題を検出するためsetupとcleanupが追加で実行されます。

重要なのは回数を無理に1回へ抑えることではなく、setup → cleanup → setupでも利用者から見た結果が壊れないEffectにすることです。

useEffectの基本形

useEffect(setup, dependencies)には、外部systemとの同期を始めるsetup関数と、その処理が読むreactive valueの一覧を渡します。setupは必要に応じてcleanup関数を返します。

useEffect(() => {
  const connection = createConnection(serverUrl, roomId);
  connection.connect();

  return () => {
    connection.disconnect();
  };
}, [serverUrl, roomId]);

Effectは次の順序で同期します。

  1. componentがpageへ追加された後にsetupする
  2. dependencyが変わったcommit後、古い値でcleanupする
  3. 新しい値でsetupし直す
  4. componentがpageから削除されると最後にcleanupする

Effectを「mount・update・unmountのcallback」として暗記するより、1つの外部systemを現在のprops・stateへ同期する独立したprocessとして考えると、dependencyとcleanupを設計しやすくなります。

空の依存配列[]で初回mount時に実行する

Effect内でcomponentのprops・state・component内で宣言した関数など、変化し得るreactive valueを読まない場合、dependencyは空配列になります。

useEffect(() => {
  const controller = new ExternalWidget();
  controller.start();

  return () => {
    controller.stop();
  };
}, []);

空配列は「dependencyを無視する記号」ではありません。「このEffectにはrenderごとに変わるreactive dependencyがない」という宣言です。

1回だけの意味は1mountにつき1回

次の操作では再mountが起こり、Effectはもう一度setupされます。

  • 条件分岐でcomponentを一度削除して再表示する
  • componentのkeyを変更する
  • 別pageへ移動して戻る
  • 開発中のFast RefreshでEffectが再同期される

「利用者1人につき必ず1回」「注文を必ず1件だけ作る」といった業務上の一意性をEffectの回数へ任せてはいけません。databaseのunique制約やidempotency keyなど、該当する層で保証します。

依存配列なし・空配列・値ありの違い

統合した旧記事で扱っていたuseEffectの第二引数は、次の3パターンに整理できます。

書き方 再実行 用途
第二引数を省略 すべてのcommit後 必要な場面は少ない
[] 通常はmount時。unmount時にcleanup 変化するreactive valueを読まない同期
[roomId] roomIdが変わったcommit後 値に合わせて外部systemを再同期

ReactはdependencyをObject.isで前回の値と比較します。配列の項目数は一定にし、[dep1, dep2]のようにinlineで書きます。

Strict ModeでuseEffectが2回動く理由

Strict Modeが有効な開発環境では、最初の本番相当setupの前に、Reactが追加のsetup + cleanup cycleを実行します。典型的にはconsoleで次の順序が見えます。

setup
cleanup
setup

これはproductionで常に2回実行するという意味ではありません。cleanupがsetupを正しく元へ戻せるかを確認し、接続・timer・event listener・fetchのrace conditionなどを開発時に見つけるstress testです。

Strict Modeを外す前にcleanupを直す

2回動くのを見てStrict Modeを削除すると、見つかった問題を隠すだけになる場合があります。利用者から見て、1回のsetupとsetup → cleanup → setupが同じ結果になるようにします。

  • subscriptionを開始したら解除する
  • event listenerを登録したら同じhandlerを削除する
  • timerを開始したらclearする
  • animationを開始したら初期状態へ戻す
  • fetch結果が古くなったらabortまたは無視する

React公式のStrictMode referenceでも、Effectごとに開発時の追加setup + cleanup cycleが説明されています。

setIntervalを初回mount時に開始する正しい例

元記事のtimer例はIDを保存しておらず、cleanupで未定義の変数を参照していました。次のようにsetInterval()の戻り値を保存します。

import { useEffect, useState } from "react";

export default function ElapsedTime() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = window.setInterval(() => {
      setSeconds((current) => current + 1);
    }, 1000);

    return () => {
      window.clearInterval(intervalId);
    };
  }, []);

  return <time>{seconds}秒経過</time>;
}

setSeconds((current) => current + 1)というupdater functionを使うため、Effectはsecondsを読みません。したがってdependencyは[]のままで正しく、秒数が変わるたびにintervalを作り直す必要もありません。

cleanupがないと何が起きるか

cleanupせずに再mountすると、古いintervalが残ったまま新しいintervalも始まります。Strict Modeの開発時追加cycleは、このような重複を早期に見つけます。

event listenerを1回登録してcleanupする

browserのnative event listenerを登録する場合も、同じ関数参照をremoveします。

useEffect(() => {
  function handleResize() {
    console.log(window.innerWidth);
  }

  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, []);

JSXのonClickなどReact eventは通常Effectで登録せず、event handlerとして直接渡します。native eventとの使い分けはReactでイベントを登録する方法で扱います。

fetchをuseEffectで実行する場合

Effectのsetup関数自体をasyncにするとPromiseが返り、cleanup関数を返せません。Effect内にasync関数を定義して呼び出しますが、それだけではrace condition対策になりません。

import { useEffect, useState } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUser() {
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const data = await response.json();
        setUser(data);
      } catch (fetchError) {
        if (fetchError.name !== "AbortError") {
          setError(fetchError);
        }
      }
    }

    loadUser();

    return () => {
      controller.abort();
    };
  }, [userId]);

  if (error) return <p>読み込みに失敗しました</p>;
  if (!user) return <p>読み込み中…</p>;
  return <p>{user.name}</p>;
}

userIdが変わった場合、古いrequestをabortしてから新しいIDを取得します。abortできない処理では、cleanupでignore = trueにし、古い結果がstateへ反映されないようにします。

実際のapplicationでは、frameworkのdata fetching、cache・deduplicationに対応したlibrary、server componentなどが適する場合があります。API取得の基本はReactで外部APIからJSONを取得する方法も参照してください。

dependencyはEffectが読む値に合わせる

dependencyは「再実行したい値を選ぶoption」ではなく、Effectが読むすべてのreactive valueを宣言します。props、state、component本体で宣言した変数・関数が対象です。

function ChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();

    return () => connection.disconnect();
  }, [serverUrl, roomId]);
}

ESLintのexhaustive-deps警告が出るとき、単に[]へ変えたり警告を抑止したりしません。dependencyを消したいなら、その値がreactiveでなくなるようコード構造を変える必要があります。

component外へ定数を移す

すべてのcomponent instanceで同じ値なら、module scopeへ移すことでrenderに依存しないことを示せます。

const serverUrl = "https://example.com";

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
}

Effect内へobjectや関数を移す

renderごとに新しく作られるobjectやfunctionは、identityが毎回変わります。Effectだけで使うならEffect内で作ると、不要なdependencyを減らせます。

無限loopが起きる条件

dependencyへstateを入れるだけで無限loopになるわけではありません。主に次の2条件が組み合わさるとloopします。

  1. Effectがstateを更新する
  2. その更新によりdependencyが毎回変わる
// 悪い例: countを読むEffectが毎回countを更新する
useEffect(() => {
  setCount(count + 1);
}, [count]);

まず「外部systemとの同期が本当に必要か」を確認します。単に別のstateから値を作るだけならEffectを削除します。

// 不要なEffect
const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// 改善: render中に計算する
const fullName = `${firstName} ${lastName}`;

render中の計算はside effectではありません。ただし、元記事にあったようにrender中に無条件でsetState()を呼ぶのは再render loopを起こすため禁止です。

useRefでStrict Modeの2回実行を抑止しない

次のguardは一見「初回だけ」に見えますが、Strict Modeが検出しようとしているcleanup不備を隠します。

// 避ける
const hasRun = useRef(false);

useEffect(() => {
  if (hasRun.current) return;
  hasRun.current = true;
  connect();
}, []);

開発時の最初のsetupで接続し、cleanupせず、2回目だけguardで止めると、接続が解放されない問題は残ります。またcomponentが本当にunmount・remountしたときの要件も満たせません。

正しくはsetupとcleanupを対にします。

useEffect(() => {
  const connection = connect();
  return () => connection.disconnect();
}, []);

元記事の「render中にrefを書き換えてside effectを1回実行する」例も削除しました。Reactはrenderを複数回呼べるため、side effectはrender外のevent handlerまたはEffectへ置きます。useRefはDOM参照や、render結果に不要なmutable valueの保持には有効ですが、Effectの設計を回避するflagではありません。

useEffectを使わないほうがよい処理

EffectはReactの外部systemと同期するためのescape hatchです。外部systemがなければ、多くの場合は不要です。

props・stateから表示値を計算する

配列のfilter、氏名の連結、合計値などはrender中に計算します。高価な計算だけ、必要に応じてuseMemoを検討します。

利用者の操作に反応する

購入、送信、download、通知など、button clickが原因の処理はevent handlerへ置きます。

function BuyButton({ productId }) {
  async function handleBuy() {
    await fetch("/api/buy", {
      method: "POST",
      body: JSON.stringify({ productId }),
    });
  }

  return <button onClick={handleBuy}>購入する</button>;
}

pageが表示されたことと、利用者が購入を確定したことは別です。購入処理をuseEffect(..., [])へ置くと、再mountや戻る操作で再実行される危険があります。

useStateの初期値を重く計算する

初回renderでstateの初期値を計算したいだけなら、lazy initializerを使います。

const [settings] = useState(() => loadInitialSettings());

これは外部systemと同期するEffectではなく、stateの初期化です。initializerはpureに保ちます。

application起動時に初期化する

componentの表示とは無関係に、browserでmoduleが読み込まれたときだけ行う初期化はcomponent外へ置ける場合があります。

if (typeof window !== "undefined") {
  initializeClientLibrary();
}

function App() {
  return <Main />;
}

server rendering、test、Hot Module Replacementではmoduleの評価回数が異なり得ます。初期化関数自体をidempotentにし、使用frameworkのentry pointやproviderがあるならそちらを優先します。

Effect Eventで最新値を読む

対応するReact環境ではuseEffectEventを使い、外部systemを再接続せずに最新のprops・stateを読む処理を分離できます。dependencyを隠す一般的な抜け道ではありません。

import { useEffect, useEffectEvent } from "react";

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification("接続しました", theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on("connected", onConnected);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
}

接続はroomIdの変更に反応し、notificationは接続時点の最新themeを読みます。Effect Eventは同じcomponent内のEffectから呼び、dependency配列には含めません。

useEffectとuseLayoutEffectの選び方

通常の外部同期はuseEffectを使います。tooltip位置の測定など、browserがpaintする前にlayoutを測って同期しないとちらつくvisual処理ではuseLayoutEffectが必要な場合があります。

両者の実行時点とserver rendering時の注意は、useEffectとuseLayoutEffectの違いで分けて解説します。

繰り返す同期処理はCustom Hookへまとめる

timer、event subscription、data source同期を複数componentで使う場合は、目的が伝わるCustom Hookへ抽出できます。

import { useEffect } from "react";

function useInterval(callback, delay) {
  useEffect(() => {
    const intervalId = window.setInterval(callback, delay);
    return () => window.clearInterval(intervalId);
  }, [callback, delay]);
}

実際にはcallback identityでintervalが再作成される点を考慮し、Effect Eventや呼び出し側の設計を組み合わせます。Custom Hookは「初回だけ実行する」という実装詳細ではなく、useOnlineStatususeChatRoomのように目的を表す名前にします。基本はReact Custom Hookの作り方で解説しています。

初回だけ実行したいときの判断表

やりたいこと 適した場所
外部widget・connection・timerをcomponent表示中だけ同期 useEffect + cleanup
props・stateから表示値を作る render中の計算
button clickでPOSTする event handler
state初期値を計算する useState lazy initializer
application全体のclient libraryを初期化する entry point・provider・module scope
利用者単位で厳密に1回だけ処理する server・database側で一意性を保証

よくある質問

空配列[]なら絶対に1回だけですか?

いいえ。通常のproductionで1mount中はprops・stateの変化では再実行しませんが、再mountすれば再実行します。Strict Modeの開発環境では追加のsetup + cleanup cycleもあります。

Strict Modeで2回動くのを止めるには?

useRef guardで隠すのではなく、setupをcleanupで完全に戻します。POSTなど再実行できない処理は、Effectではなく原因となるevent handlerへ移します。

dependency警告を消すため[]へ変えてよいですか?

いけません。Effectが読むreactive valueとdependencyを一致させます。警告を消すには値や関数を外へ移す、Effectを分割する、updater functionを使うなど、コード構造を修正します。

fetchは初回だけなら[]でよいですか?

URLやuser IDがprops・stateで変わるなら、それらをdependencyへ含めます。cleanupで古いrequestをabortまたは無視し、race conditionを防ぎます。

useRefでhasRunを管理するのは常に間違いですか?

ref自体は有用ですが、Strict ModeのEffect再実行を止める目的では問題を隠します。render中のside effectやref書き換えも避けてください。

useEffectはserver renderingで動きますか?

Effectはclient側でのみ実行され、server rendering中には動きません。初期HTMLに必要なdataはframeworkのserver data APIなどで用意します。

まとめ

useEffectを初回mount時に実行する基本形はuseEffect(setup, [])です。ただし、空配列はapplication全体で厳密に1回を保証するものではありません。

  • Effectは外部systemとの同期に使う
  • setupした処理をcleanupで元へ戻す
  • dependencyはEffectが読むreactive valueと一致させる
  • Strict Modeの追加cycleをuseRef guardで隠さない
  • render計算とevent handlerにはEffectを使わない
  • fetchはabort・ignore・cacheでrace conditionへ対応する

「何回呼ばれるか」だけでなく、「何と同期し、いつ解除するか」を基準に設計すると、再mount・dependency変更・Strict Modeでも壊れにくいEffectになります。