ReactのuseEffectには「クリーンアップ関数」という後始末の仕組みがあります。
この関数は、useEffectが再実行されたり、コンポーネントがアンマウントされたときに呼ばれます。

この仕組み自体はシンプルですが、実装を誤るとイベントリスナーの解除漏れやタイマーのゾンビ化、外部接続の放置といった問題が生じ、構造が脆弱になります。

クリーンアップ関数とは何か

useEffectが返す関数が「クリーンアップ関数」です。これは前回の副作用を無効化・破棄する目的で実行されます。

基本形
useEffect(() => {
  const timer = setInterval(() => { ... }, 1000);

  return () => {
    clearInterval(timer); // ← クリーンアップ関数
  };
}, []);

この構造を正しく理解していないと、副作用が積み重なり、意図しない挙動が発生します。

よくある破棄漏れ

NGパターン:リスナーの解除忘れ
useEffect(() => {
  window.addEventListener('resize', onResize);
}, []);
Comment
@Reviewer: addEventListenerを行っているにもかかわらず、removeEventListenerでの解除処理がありません。これは不要なイベントが積み重なる温床になります。
改善例
useEffect(() => {
  window.addEventListener('resize', onResize);

  return () => {
    window.removeEventListener('resize', onResize);
  };
}, []);

クリーンアップ関数の責務と分離設計

次のような構造は「やりすぎなクリーンアップ」としてレビュー指摘対象になり得ます。

責務過剰なクリーンアップ
useEffect(() => {
  connectSocket();
  startPolling();

  return () => {
    disconnectSocket();   // OK
    stopPolling();        // グローバル制御?要分離検討
    resetState();         // 状態リセット?Hookでやるべき
  };
}, []);

すべてをここで行うと、useEffectの外部依存性が高くなり、Hook自体が密結合化します。

Hookへの責務移譲という設計判断

イベント破棄や解除処理をカスタムHook側へ移譲することで、コンポーネントの責務を軽量化できます。

責務分離の例
function useWindowResize(handler: () => void) {
  useEffect(() => {
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, [handler]);
}
コンポーネント側は簡潔に
useWindowResize(() => {
  setSize(window.innerWidth);
});

クリーンアップ設計のレビュー観点

図解:構造的責務の分離

UML Diagram

まとめ

クリーンアップ関数は単なる後始末ではなく、構造上の責務分担を表すシグナルでもあります。

責務が集中していたらHook化を検討し、破棄忘れがないかレビューで丁寧に見ていくことが、長期的な安定性につながります。

Reactは副作用に寛容である一方、破棄の設計を怠ると静かにバグが育ちます。
レビューアーの視点で、構造として破棄が整理されているかを評価することが重要です。