React useEffect の cleanup が期待通り呼ばれない
useEffect が返した関数(cleanup)はアンマウント時と次回 effect 実行直前に走る。
React 18 の Strict Mode では開発時にマウント直後にも一度実行される。
cleanup が呼ばれないように見える場合は、依存配列の漏れ、effect 内で例外を投げた、登録時と解除時で関数参照が違う、のいずれかを疑う。
#useeffect#cleanup#strict-mode#lifecycle
公開:
要約
useEffect で return () => { ... } を返した関数は cleanup と呼ばれ、(1) コンポーネントのアンマウント時、(2) 依存配列の値が変わって effect が再実行される直前、の 2 タイミングで呼ばれる。
「呼ばれない」ように見える原因の多くは、依存配列のずれによって effect 自体が再実行されておらず、登録した subscription が古いまま残っている、または React 18 Strict Mode の二重実行を別の問題と誤認しているケース。
よくある原因
- 依存配列が
[]で固定されており、props が変わっても effect が再実行されない(= cleanup も走らない)。
当然 stale な subscription が残る。 - React 18 Strict Mode は dev 時に意図的に「マウント → アンマウント → 再マウント」を行う。
cleanup が 2 回走ったように見えるが、これは「冪等性チェック」のための仕様。 - effect の同期処理中で例外を投げて return まで到達せず、cleanup 関数が登録されないままになる。
addEventListener('click', () => f())のように登録ごとに別の無名関数を渡し、removeEventListenerで別の関数参照を渡してしまい解除できない。
解決策
1. cleanup と登録で同じ参照を使う
useEffect(() => {
const handler = (e: MouseEvent) => console.log(e.clientX);
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);handler を effect 内で定義し、登録と解除で同じ変数を参照する。
2. 依存配列を正しく宣言する
eslint-plugin-react-hooks の exhaustive-deps ルールを有効化し、警告に従って依存を埋める。
必要な値を意図的に外したいときも、コメントで根拠を残す。
3. Strict Mode の二重実行を前提に書く
useEffect(() => {
const id = setInterval(() => tick(), 1000);
return () => clearInterval(id);
}, []);setInterval と clearInterval が対になっていれば、二重実行されてもタイマーは 1 本に収束する。
詳細は 公式ドキュメント (useEffect)。
4. AbortController で fetch / listener を一括解除
useEffect(() => {
const ctrl = new AbortController();
fetch("/api/data", { signal: ctrl.signal })
.then(r => r.json())
.then(setData)
.catch(e => { if (e.name !== "AbortError") console.error(e); });
return () => ctrl.abort();
}, []);非同期処理を cleanup できるようにしておくと「アンマウント後の setState」警告も消える。