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は次の順序で同期します。
- componentがpageへ追加された後にsetupする
- dependencyが変わったcommit後、古い値でcleanupする
- 新しい値でsetupし直す
- 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します。
- Effectがstateを更新する
- その更新により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は「初回だけ実行する」という実装詳細ではなく、useOnlineStatusやuseChatRoomのように目的を表す名前にします。基本は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になります。