useEffectのクリーンアップ関数設計:副作用の後始末はどこまで責務か
ReactコンポーネントにおけるuseEffect
は副作用処理の代表的な構造ですが、そのクリーンアップ関数は後回しにされがちです。レビュー現場では、「クリーンアップ処理が正しく記述されていない」「副作用が残留してメモリリークやイベント多重発火が発生する」といった指摘が頻繁に登場します。
本記事では、useEffect
の中に定義されるクリーンアップ関数の設計責任をレビュー観点から見直し、正しい副作用の終息と構造的な責務分離の指針を明確にしていきます。
useEffectのクリーンアップ構造の基本
useEffect(() => {
const handler = () => console.log("resized");
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}, []);
このように、useEffect
内で副作用の登録と解除を一対で記述するのがクリーンアップの基本構造です。
useEffect
の戻り値として返す関数は、コンポーネントのアンマウント時や依存値の変化による再実行前に呼び出される「後始末処理」の関数です。
よくあるレビュー指摘:イベントやタイマーが残る構造
useEffect(() => {
const intervalId = setInterval(() => {
console.log("polling");
}, 1000);
}, []);
@Reviewer: `setInterval` のクリーンアップが存在していません。アンマウント後も動作し続け、メモリリークやバグの原因になります。`clearInterval` による解除処理を忘れずに。
このように、非同期系・永続系の副作用(イベント、タイマー、サブスクリプション)は必ず解除処理が必要です。
クリーンアップ責務の分離とカスタムHook化
責務が肥大化したuseEffect
内部にすべての副作用とクリーンアップを詰め込むと、レビューもしづらくなります。
以下は責務を分離し、イベント登録の構造を明示したカスタムHookの例です。
export function useResizeHandler(handler: () => void) {
useEffect(() => {
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}, [handler]);
}
useResizeHandler(() => {
console.log("resized");
});
@Reviewer: クリーンアップ責務がHook内に封じ込められており、利用側は目的のみ記述することで誤用が防がれています。
複数副作用を1つのuseEffectにまとめすぎない
useEffect(() => {
const resizeHandler = () => console.log("resize");
const scrollHandler = () => console.log("scroll");
window.addEventListener("resize", resizeHandler);
window.addEventListener("scroll", scrollHandler);
return () => {
window.removeEventListener("resize", resizeHandler);
// scrollHandlerの解除を忘れている
};
}, []);
副作用を一つのuseEffect
に集約しすぎると、クリーンアップ漏れが起こりやすくなります。副作用は種類ごとに分け、個別にuseEffect
を用いる構造の方が安全です。
可視化:副作用とクリーンアップの対応構造
クリーンアップ関数のレビュー観点
まとめ
useEffect
の副作用設計において、クリーンアップ関数は単なる付録ではなく、等しく設計責務を負う構造です。レビューアーとしては、「何が永続し、何を解除すべきか」を見極め、副作用がアプリの寿命を越えて残らないようにチェックを徹底する必要があります。
副作用は単体テストで検出しづらく、クリーンアップ漏れは後から深刻なバグになることもあるため、レビュー時の確認が唯一の防衛線になる場面も少なくありません。