こんにちは。現場の最前線でコードと格闘している皆さん、お疲れ様です。
Reactの `useEffect` 、便利ですよね。でも、実務で一番ハマるのが「副作用の片付け」――つまりクリーンアップ関数です。特に「コンポーネントが消えるときに、本当にタイマーやイベントリスナーが解放されているのか?」という不安を抱えたままリリースするのは、プロとして避けたいところです。
今日は、この「クリーンアップ関数のテスト」という、現場で意外と疎かにされがちなトピックを、泥臭いリアリティを交えて解説します。
—
なぜクリーンアップのテストが「沼」なのか
まず、前提として理解しておきたいのは、「クリーンアップ関数はReactが管理するブラウザのイベントループの隙間で動く」という事実です。
アンマウント直前にReactがこれを実行するわけですが、テストコードを書く際、「コンポーネントがアンマウントされたこと」をどうやって担保するか。DOMから消えるのを待つだけでは不十分なケースが多いのです。Reactのレンダリングサイクルと、テストランナー(Jest/Vitestなど)の実行タイミングのズレをどう制御するかが、このテストの肝になります。
実践:クリーンアップ関数の検証パターン
現場で私がよく使う、最も堅実なアプローチを紹介します。ここでは、`addEventListener` を例に取ります。
1. 依存関係の注入(モック化)
直接 `window` に触れるとテストが壊れやすくなるので、モック関数を作成し、それが呼び出されたかどうかを追跡します。
2. 実行環境の制御
React Testing Libraryの `unmount` メソッドを呼ぶことで、Reactのアンマウント処理を明示的にトリガーします。
import { render } from ‘@testing-library/react’;
import { useEffect } from ‘react’;
// テスト対象のコンポーネント
const EventListenerComponent = ({ onCleanup }) => {
useEffect(() => {
const handleResize = () => console.log(‘resized’);
window.addEventListener(‘resize’, handleResize);
// これがテストしたいクリーンアップ関数
return () => {
window.removeEventListener(‘resize’, handleResize);
onCleanup(); // テスト用に通知を送る
};
}, [onCleanup]);
return
;
};
describe(‘クリーンアップ関数の検証’, () => {
it(‘アンマウント時に適切にイベントリスナーが解除されること’, () => {
// スパイ関数を用意
const cleanupSpy = jest.fn();
// window.removeEventListener をスパイする
const removeSpy = jest.spyOn(window, ‘removeEventListener’);
const { unmount } = render(
// まだアンマウントしていないので呼ばれていないはず
expect(cleanupSpy).not.toHaveBeenCalled();
// アンマウント実行
unmount();
// クリーンアップ関数が実行されたか確認
expect(cleanupSpy).toHaveBeenCalledTimes(1);
// 実際に window からリスナーが削除されたか確認
expect(removeSpy).toHaveBeenCalledWith(‘resize’, expect.any(Function));
// メモリリークを防ぐためスパイを解除
removeSpy.mockRestore();
});
});
—
現場で膝を打つための「3つの極意」
1. 「副作用の副作用」を疑え
クリーンアップ関数内で外部の状態を操作している場合、テストが終わった後にそのモックが別のテストケースに悪影響を及ぼすことがあります。`afterEach(() => jest.restoreAllMocks())` は必ず忘れないでください。
2. `useEffect` の依存配列は「嘘」をつかない
クリーンアップ関数が呼び出されるのは、コンポーネントのアンマウント時だけではありません。依存配列の中身が変わったときにも呼び出されます。
テストを書く際は、「アンマウント時」だけでなく、「依存配列の変更時にクリーンアップが走るか」というケースも網羅すると、コードの品質が一段階上がります。
3. DOMの状態に頼りすぎない
「画面から消えた=クリーンアップが走った」と見なすのは危険です。Reactのレンダリングサイクルは非同期的な要素を含みます。DOMの存在チェックではなく、「クリーンアップ関数内で定義したスパイ関数が呼ばれたか」という、ロジックの実行結果を検証するのが、最も壊れにくいテストの書き方です。
—
最後に:なぜここまでやるのか
「たかがクリーンアップ」と思うかもしれません。しかし、大規模なSPAで、メモリリークによるパフォーマンス低下や、謎のイベントリスナーによる重複実行に悩まされた経験はありませんか?
クリーンアップのテストは、単なるコードカバレッジ稼ぎではなく、「コンポーネントのライフサイクルを我々エンジニアが完全に手中に収めている」という証明です。
テストコードは、未来の自分やチームメンバーへの「このコンポーネントは行儀よく片付けを行う」という約束状でもあります。ぜひ、次のプルリクエストから、この視点を取り入れてみてください。
もし、さらに深い「React 18のStrict Modeによるダブルマウント時の挙動」などについて気になったら、いつでも聞いてください。現場の泥臭い話、まだまだ沢山ありますから。

コメント