やあ、Reactの世界へようこそ!
Reactの学習、順調に進んでいますか?きっと、コンポーネントを組み立てたり、状態を管理したり、新しい発見の連続でワクワクしていることでしょう。でも、その中で「useEffect」というフックに出会って、「うーん、これはちょっと奥が深いぞ…」と感じた方もいるかもしれませんね。特に、その中に出てくる「クリーンアップ関数」ってやつ、ちょっと難しく感じていませんか?
大丈夫ですよ!今日はお祭り騒ぎの後の「お片付け」に例えながら、このクリーンアップ関数、そしてそのお片付けがちゃんと行われたかをテストで確認する方法について、優しく、そして現場の知恵を交えながら解説していきます。
僕らはただ動くものを作るだけじゃなくて、長く安定して、そして安心して使えるものを作りたい。そのための大切なステップが、この「クリーンアップ関数のテスト」なんです。さあ、一緒にこのReactの奥深い世界を覗いていきましょう!
—
🧹 クリーンアップ関数って、そもそも何者?(お祭り騒ぎの後のお片付け)
まずは、クリーンアップ関数がどんな役割を持っているのか、簡単におさらいしましょう。
Reactのコンポーネントは、画面に表示されたり(マウント)、更新されたり、そして画面から消えたり(アンマウント)と、まるで人生のように様々なライフサイクルを持っています。
`useEffect`は、このコンポーネントのライフサイクルに合わせて、「何か処理を実行したいな」というときに使うフックでしたね。例えば、
- 画面に表示されたら、タイマーをスタートしたい!
- 画面に表示されたら、外部APIからデータを取ってきたい!
- 画面に表示されたら、ウィンドウのリサイズを監視したい!
などなど、コンポーネントの「表示」をきっかけに始まる、いろんな処理があります。
🚨 でも、始めたら、必ず終わりも意識してほしいんです
そう、ここが今日の主役、クリーンアップ関数の登場です。
例えば、お祭り会場をイメージしてみてください。
1. お祭り開始!(コンポーネントマウント)
- 屋台を並べる
- 提灯を飾る
- スピーカーで音楽を流す
- お客さんが楽しく過ごす
- (これが`useEffect`で実行される処理たち)
2. お祭り終了!(コンポーネントアンマウント)
- 屋台を片付ける
- 提灯を回収する
- スピーカーを止めて、電源を切る
- (これがクリーンアップ関数で実行される処理たち)
もし、お祭り終了後に片付けをしなかったらどうなるでしょう?ゴミは散らかり放題、屋台は出しっぱなし、スピーカーは鳴りっぱなし…大変なことになりますよね。
Reactの世界でも同じです。コンポーネントが画面から消えたのに、まだタイマーが動き続けていたり、使わないイベントリスナー(ウィンドウのリサイズ監視など)が残っていたりすると、
- メモリリーク:使っていないはずのメモリが解放されず、アプリの動作がだんだん重くなる。まるで、水道の蛇口を閉め忘れて水が出っぱなしになっているようなものです。
- 予期せぬ挙動:消えたはずのコンポーネントに関連する処理が裏で動き続けて、エラーを起こしたり、他のコンポーネントの邪魔をしたりする。
こんな困った事態に陥ってしまうんです。だからこそ、`useEffect`で始めた処理は、必ずクリーンアップ関数で後片付けをする、これがReact開発の鉄則であり、プロフェッショナルな仕事の証なんです。
クリーンアップ関数は、`useEffect`のコールバック関数が返す関数として定義します。
import React, { useEffect, useState } from ‘react’;
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// ① ここが「お祭り開始!」の処理(副作用)
// 例えば、1秒ごとにカウントアップするタイマーをセット
const timerId = setInterval(() => {
setCount(prevCount => prevCount + 1);
console.log(‘カウントアップ中…’, count); // countの値はuseEffectが実行された時点のものになることに注意
}, 1000);
console.log(‘✨ タイマーを開始しました!’);
// ② ここが「お祭り終了!」のお片付け(クリーンアップ関数)
// コンポーネントが画面から消える時などに呼ばれます
return () => {
clearInterval(timerId); // タイマーを停止して、リソースを解放
console.log(‘🧹 タイマーを停止しました!’);
};
}, []); // 依存配列が空なので、マウント時に一度だけ実行され、アンマウント時にクリーンアップされます
return (
MyComponent
カウント: {count}
このコンポーネントが画面から消えると、タイマーが停止します。
);
}
export default MyComponent;
—
🧐 なぜクリーンアップ関数を「テスト」するの?(ここが大事!)
「ちゃんとクリーンアップ関数を書いたから、大丈夫でしょう?」
そう思われるかもしれません。僕も昔はそうでした。でもね、現場では「動いているから大丈夫」が、実は一番危険な考え方だったりするんです。
見えないエラーの怖さ
想像してみてください。あなたは料理をしていて、ガスコンロの火を消し忘れて部屋を出てしまいました。火は燃え続けているけど、部屋の外からは見えない。しばらくして、何か焦げ臭い匂いがしてきて、ようやく「しまった!」となる。
メモリリークやイベントリスナーの解除忘れもこれに似ています。開発中にちょっと動かしただけでは気づかない。でも、ユーザーが長時間アプリを使ったり、何度もコンポーネントを行ったり来たりするうちに、だんだんアプリが重くなったり、予期せぬエラーが出始めたりするんです。
テストは「保険」であり「安心材料」
だからこそ、僕らはテストを書きます。
テストは、僕らが書いたコードが「期待通りに動いているか」を自動でチェックしてくれる、いわば「コードのための保険」のようなものです。
特に、クリーンアップ関数が「コンポーネントが消えたときに、ちゃんと後片付けをしてくれるか」をテストすることは、アプリの安定性を保つ上で、とてつもなく大切なんです。
「このコンポーネントは、どんなに複雑な動きをしても、最後は必ず綺麗に後片付けをしてくれる」という確信があれば、安心して新しい機能を追加したり、リファクタリング(コードの整理)を進めたりできますよね。これは開発者にとって、何物にも代えがたい安心感を与えてくれるんです。
—
🚀 さあ、実践!クリーンアップ関数をテストしてみよう
それでは、実際にクリーンアップ関数が正しく呼ばれることをテストで確認してみましょう。Reactのテストには、主に「React Testing Library (RTL)」というライブラリを使います。RTLは、ユーザーがアプリケーションをどのように使うか、という視点に立ってテストを書くことを推奨している、とても素晴らしいライブラリです。
今回は、イベントリスナーを登録し、コンポーネントがアンマウントされたときにそのリスナーが解除される、という簡単なコンポーネントを例にテストを書きます。
1. テスト対象のコンポーネントを用意しよう
まず、テストしたいコンポーネントを用意します。今回は、ウィンドウのリサイズを監視する簡単なコンポーネントです。
`src/components/MyComponent.tsx`
import React, { useEffect } from ‘react’;
interface MyComponentProps {
// テスト用に、クリーンアップ関数が実行されたことを外部に通知するためのコールバック関数
// 実際のプロダクトコードでは通常は不要ですが、テストの検証をシンプルにするために今回だけ登場します。
onCleanupCalled?: () => void;
}
const MyComponent: React.FC
useEffect(() => {
// コンポーネントがマウントされた時(表示された時)に実行される処理
// ここでは、ウィンドウのリサイズイベントを監視するリスナーを登録しています。
const handleResize = () => {
console.log(‘ウィンドウがリサイズされました!’);
// 実際にはここで何かしらの処理(UIの調整など)を行うことが多いでしょう。
};
window.addEventListener(‘resize’, handleResize);
console.log(‘✨ リサイズリスナーを登録しました!’);
// クリーンアップ関数は、コンポーネントがアンマウントされる時(消える時)や、
// 依存配列が変わってuseEffectが再実行される前に呼ばれます。
return () => {
window.removeEventListener(‘resize’, handleResize);
console.log(‘🧹 リサイズリスナーを解除しました!’);
// テスト用に、クリーンアップ関数が呼ばれたことを外部に通知します。
// この関数が呼ばれることで、テスト側からクリーンアップが実行されたことを検知できます。
if (onCleanupCalled) {
onCleanupCalled();
}
};
}, []); // 依存配列が空なので、マウント時に一度だけ実行され、アンマウント時にクリーンアップされます
return (
MyComponent
このコンポーネントが表示されている間、ウィンドウのリサイズを監視しています。
コンポーネントが消えたら、監視をやめます。
(ブラウザの幅を変えて、開発者ツールのコンソールを見てみてください!)
);
};
export default MyComponent;
このコンポーネントには、`onCleanupCalled`というちょっと特殊なプロップがあります。これは、テストをシンプルにするために一時的に追加したもので、クリーンアップ関数が実行されたときに、そのことをテストコードに通知する役割を持っています。実際のアプリではこんなプロップはまず使いませんから、ご安心を!
2. テストコードを書いてみよう
次に、このコンポーネントのテストコードを書きます。
`src/components/MyComponent.test.tsx`
import React from ‘react’;
import { render, screen } from ‘@testing-library/react’;
import MyComponent from ‘./MyComponent’;
describe(‘MyComponent’, () => {
it(‘コンポーネントがアンマウントされたときにクリーンアップ関数が正しく呼び出されること’, () => {
// ① クリーンアップ関数が呼ばれたことを確認するための「目印」となるモック関数を用意します。
// jest.fn()は、その関数が「いつ、何回、どんな引数で呼ばれたか」を記録してくれる便利な機能です。
const mockCleanupCallback = jest.fn();
// ② MyComponentをレンダリングします。
// render関数は、テスト対象のコンポーネントを仮想DOMに描画し、それを操作するための色々なヘルパーを返してくれます。
// ここで特に注目したいのは、render関数が返すオブジェクトに含まれる ‘unmount’ 関数です。
// これはコンポーネントを仮想DOMから「取り除く」、つまりアンマウントする役割を持っています。
const { unmount } = render(
);
// ③ この時点では、コンポーネントはまだ表示されているので、クリーンアップ関数は呼ばれていないはずです。
// toHaveBeenCalledTimes(0) は、「この関数が0回呼び出されたこと」を検証します。
expect(mockCleanupCallback).toHaveBeenCalledTimes(0);
// ④ さあ、ここでコンポーネントをアンマウントしてみましょう!
// unmount()を呼び出すことで、Reactはコンポーネントを画面から消し、
// その際にuseEffectのクリーンアップ関数が実行されます。
unmount();
// ⑤ コンポーネントがアンマウントされた後、クリーンアップ関数が呼ばれたことを確認します。
// toHaveBeenCalledTimes(1) は、「この関数が1回だけ呼び出されたこと」を検証します。
// これで、クリーンアップが正しく実行されたことが証明できましたね!
expect(mockCleanupCallback).toHaveBeenCalledTimes(1);
});
});
コードの解説
1. `jest.fn()`で「目印」を用意
`jest.fn()`は、Jestというテストフレームワークが提供する、とても便利なモック関数を作る機能です。これを使うと、作った関数が「何回呼ばれたか」「どんな引数で呼ばれたか」などを後から確認できるようになります。今回は、クリーンアップ関数が呼ばれたことを検知するための「目印」として使っています。
2. `render`と`unmount`
React Testing Libraryの`render`関数は、コンポーネントを仮想のDOMに描画します。この`render`が返すオブジェクトには、`unmount`という関数が含まれています。この`unmount`関数を呼び出すことで、コンポーネントが強制的に画面から取り除かれ、その結果`useEffect`のクリーンアップ関数が実行されます。これが今日のテストの肝です!
3. `expect(…).toHaveBeenCalledTimes(0)`
コンポーネントがまだ表示されている状態(アンマウント前)では、クリーンアップ関数は呼ばれていないはずなので、`mockCleanupCallback`が0回しか呼ばれていないことを確認します。
4. `unmount()`の呼び出し
ここで実際にコンポーネントをアンマウントし、クリーンアップ関数を実行させます。
5. `expect(…).toHaveBeenCalledTimes(1)`
アンマウント後には、クリーンアップ関数が1回だけ呼ばれているはずです。これで、僕らが期待した通りに後片付けが実行されたことをテストで証明できた、というわけです!
テストを実行してみよう
ターミナルで以下のコマンドを実行すると、テストが走ります。
npm test
または
yarn test
もし全てが正しく書けていれば、以下のような結果が表示されるはずです。
PASS src/components/MyComponent.test.tsx
MyComponent
✓ コンポーネントがアンマウントされたときにクリーンアップ関数が正しく呼び出されること (XX ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: XX s
やった!これで、あなたのコンポーネントが、ちゃんと後片付けをしてくれるお利口さんであることが証明されましたね。
—
💡 テスト設計のヒントと注意点
今回のテストはシンプルでしたが、実際の現場ではもっと複雑なクリーンアップ関数に出会うこともあります。そんな時のために、いくつかヒントをお伝えしますね。
- 何のためにテストするのかを常に意識する:
ただテストを書くのではなく、「このクリーンアップが動かなかったら、どんな問題が起きるだろう?」と想像してみてください。その問題を未然に防ぐためにテストを書く、という意識が大切です。
- 複雑なロジックは小さな関数に切り出す:
`useEffect`の中身が長くなりがちなら、クリーンアップ関数も含めて、そのロジックを別の小さな関数に切り出しましょう。そうすることで、テストも書きやすくなりますし、コードも読みやすくなります。
- 非同期処理のクリーンアップも忘れずに:
`setTimeout`や`fetch`のような非同期処理を`useEffect`内で始めた場合も、クリーンアップ関数で`clearTimeout`を呼んだり、`AbortController`を使ってリクエストをキャンセルしたりする必要があります。これらも今回と同じように、`jest.fn()`でキャンセル処理が呼ばれたことを検証できますよ。
- React Testing Libraryは「ユーザー視点」:
RTLは、コンポーネントの内部実装を直接テストするのではなく、ユーザーが「画面を見て、操作する」ようにテストを書くことを促します。今回の例では、`onCleanupCalled`というプロップで内部の動きを外から観察しましたが、より高度なテストでは、`jest.spyOn`を使って`window.removeEventListener`のようなグローバルな関数が呼ばれたことを直接監視することも可能です。ですが、初学者のうちは、今回のようにシンプルにクリーンアップ関数の実行を検証するアプローチで十分です。
—
🎉 まとめ:これであなたも「後片付けの達人」!
今日の学びを振り返ってみましょう。
- `useEffect`のクリーンアップ関数は、コンポーネントが消えるときの後片付け役。メモリリークや予期せぬエラーを防ぐ、縁の下の力持ちでした。
- クリーンアップ関数が正しく動くことをテストで確認するのは、アプリの安定性と開発者の安心を確保するための大切な「保険」です。
- React Testing Libraryの`render`と`unmount`、そしてJestの`jest.fn()`を使えば、クリーンアップ関数が正しく呼び出されたかをシンプルに検証できます。
React開発は、ただコードを書くだけじゃなく、未来のことも考えて設計していく、とてもクリエイティブな仕事です。今回学んだクリーンアップ関数のテストは、まさにその「未来を考える」ための大切なスキルの一つ。
最初は難しく感じるかもしれませんが、これであなたもReactの「後片付けの達人」への一歩を踏み出しました。大丈夫、一歩一歩着実に進んでいけば、必ず最高のReact開発者になれますよ!
これからも一緒に、React開発を楽しみ、素晴らしいアプリケーションを作っていきましょう!応援しています!

コメント