useEffect cleanup関数の正しい責務分離
Reactの useEffect
では、ライフサイクル終了時に呼ばれるクリーンアップ関数(return
で返す関数)を記述することができます。しかし、現場でコードをレビューしていると、このクリーンアップ関数に不要な処理や責任範囲外の処理が含まれている例が多く見られます。
本記事では、レビューアーの立場から「useEffectのクリーンアップ関数の構造的な責務」を明確にし、何を記述すべきで何を記述すべきでないか、構造レベルで判断する指針を示します。
useEffectのクリーンアップ関数とは何か
React公式ドキュメントによれば、useEffect
に返された関数は「コンポーネントがアンマウントされる際」または「次のEffectが実行される前」に実行される処理です。
この構造は主にリソースの破棄やイベント解除を目的としており、クリーンアップ関数に新たな副作用を記述することは意図されていません。
useEffect(() => { return () => { ... } }, [])
という形式で記述される、Effectの終了時に実行される関数。イベントリスナーの解除、タイマーの停止、WebSocketの切断など、リソース解放責任を担う。
クリーンアップ関数が必要な典型例
イベントリスナーの登録・解除
useEffect(() => {
const handler = () => console.log("resize");
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}, []);
@Reviewer: 登録・解除がセットになっており、責務が明確です。クリーンアップ関数として適切です。
タイマーやintervalの停止処理
useEffect(() => {
const id = setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(id);
}, []);
このように、「登録 → 削除」「開始 → 停止」の対で動く処理は、クリーンアップ関数に責務を持たせるのが構造的に明快です。
クリーンアップ関数に不要な責務を持たせている例
状態更新のような新たな副作用
useEffect(() => {
return () => {
setFlag(false); // ← 状態更新は副作用
};
}, []);
@Reviewer: クリーンアップ関数での状態更新は、設計意図が明確でない限り避けるべきです。特にアンマウント中のstate更新はReact 18のStrictモードで警告対象となる可能性があります。
データ保存や通信処理のような意図不明の責務
useEffect(() => {
return () => {
fetch("/api/exit", { method: "POST" }); // ← 誰がいつ呼ぶのか曖昧
};
}, []);
このような構造は、「なぜここでこの副作用を実行するのか」が読者に伝わらず、再利用性・テスト性ともに劣化します。
レビューで見落とされがちなクリーンアップの欠落
WebSocketや外部接続の未解放
useEffect(() => {
const socket = new WebSocket("wss://...");
socket.onmessage = handleMessage;
}, []);
@Reviewer: WebSocketの接続は明示的に `close()` を行わないと接続が残存します。クリーンアップ関数で `socket.close()` を必ず記述すべきです。
イベント系やリソース系のEffectには「常にクリーンアップが必要」と見るのがレビューの基本スタンスです。
複数副作用の混在とクリーンアップの整理指針
1つの useEffect
に複数の副作用とクリーンアップが混在していると、責務が見えにくくなります。
useEffect(() => {
const socket = new WebSocket("wss://...");
const handler = () => console.log("resize");
window.addEventListener("resize", handler);
return () => {
socket.close();
window.removeEventListener("resize", handler);
};
}, []);
@Reviewer: 複数の副作用が1つのEffectに混在しているため、責務の追跡が難しくなります。通信処理とDOM操作は責務が異なるため、Effectを分離すべきです。
可視化:Effectとクリーンアップの責務対応図
Hookに抽出されたEffectとクリーンアップの構造判断
カスタムHookの中で副作用とクリーンアップが扱われるケースも多くなっています。レビューアーはその責務をHook外の呼び出し元から読み取れるかどうかを重視すべきです。
function useWebSocket(url: string) {
useEffect(() => {
const ws = new WebSocket(url);
return () => ws.close();
}, [url]);
}
@Reviewer: `url` の変更でWebSocketの接続が切り替わる構造は明確ですが、利用側が再接続の責任を持つべきかどうかも検討しておくと良いです。
クリーンアップ関数に関するレビュー観点まとめ
useEffect
のクリーンアップ関数は、コードの「終了の設計」です。レビューアーは「何が始まり、どう終わるか」を責務単位で見通せる構造を支持し、不要な副作用の混入や抜け漏れを未然に防ぐ役割を担います。