できない.dev

React useEffect の cleanup が期待通り呼ばれない

useEffect が返した関数(cleanup)はアンマウント時と次回 effect 実行直前に走る。
React 18 の Strict Mode では開発時にマウント直後にも一度実行される。
cleanup が呼ばれないように見える場合は、依存配列の漏れ、effect 内で例外を投げた、登録時と解除時で関数参照が違う、のいずれかを疑う。

#useeffect#cleanup#strict-mode#lifecycle

公開:

要約

useEffectreturn () => { ... } を返した関数は cleanup と呼ばれ、(1) コンポーネントのアンマウント時、(2) 依存配列の値が変わって effect が再実行される直前、の 2 タイミングで呼ばれる。
「呼ばれない」ように見える原因の多くは、依存配列のずれによって effect 自体が再実行されておらず、登録した subscription が古いまま残っている、または React 18 Strict Mode の二重実行を別の問題と誤認しているケース。

よくある原因

  1. 依存配列が [] で固定されており、props が変わっても effect が再実行されない(= cleanup も走らない)。
    当然 stale な subscription が残る。
  2. React 18 Strict Mode は dev 時に意図的に「マウント → アンマウント → 再マウント」を行う。
    cleanup が 2 回走ったように見えるが、これは「冪等性チェック」のための仕様。
  3. effect の同期処理中で例外を投げて return まで到達せず、cleanup 関数が登録されないままになる。
  4. 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-hooksexhaustive-deps ルールを有効化し、警告に従って依存を埋める。
必要な値を意図的に外したいときも、コメントで根拠を残す。

3. Strict Mode の二重実行を前提に書く

useEffect(() => {
  const id = setInterval(() => tick(), 1000);
  return () => clearInterval(id);
}, []);

setIntervalclearInterval が対になっていれば、二重実行されてもタイマーは 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」警告も消える。

この記事は役立ちましたか?