Reactでタイマーを実装する際、useEffectとsetIntervalを組み合わせるパターンはよく使われます。
しかし、クリーンアップ処理を怠るとメモリリークが発生したり、意図しない動作になることがあります。
本記事では、以下のコードをもとにuseEffectを使ったタイマー実装の仕組みをまとめました。
useEffect(() => {
if (isRunning) {
const intervalId = setInterval(() => {
setTime((time) => time + 1)
}, 1000)
return () => {
clearInterval(intervalId)
}
}
}, [isRunning])
このコードの目的は、isRunningがtrueの間、1秒ごとにtimeを+1し続けるタイマーを実装することです。
💡各部分の詳細解説
1. useEffectの依存配列 [isRunning]
useEffect(() => {
// ...
}, [isRunning]) // ← 依存配列
依存配列にisRunningを指定することで、isRunningの値が変化するたびにエフェクトが再実行されます。
| isRunningの変化 | エフェクトの動作 |
|---|---|
false → true
|
エフェクトが再実行される(タイマー開始) |
true → false
|
クリーンアップ後、エフェクトが再実行される(タイマー停止) |
2. setIntervalでタイマーを開始
if (isRunning) {
const intervalId = setInterval(() => {
setTime((time) => time + 1)
}, 1000)
}
-
isRunningがtrueのときだけsetIntervalを実行します -
setIntervalは第2引数のミリ秒(ここでは1000ms = 1秒)ごとに第1引数の関数を繰り返し呼び出します - 戻り値の
intervalIdは、後でタイマーを止めるために使います
3. 関数型更新 setTime((time) => time + 1)
setTimeの書き方には2通りあります。
// ❌ 直接値を渡す(バグが起きやすい)
setTime(time + 1)
// ✅ 関数型更新(推奨)
setTime((time) => time + 1)
なぜ関数型更新を使うのか?
setIntervalのコールバック内では、クロージャによって最初にキャプチャされたtimeの値(古い値) を参照し続けてしまいます。
関数型更新を使うと、Reactが常に最新の状態を引数として渡してくれるため、古い値を参照するバグを防げます。
4. クリーンアップ関数 return () => { clearInterval(intervalId) }
return () => {
clearInterval(intervalId)
}
useEffectから関数をreturnすることで、クリーンアップ関数を登録できます。
クリーンアップ関数は以下のタイミングで自動的に呼ばれます。
| タイミング | 説明 |
|---|---|
isRunningが変化したとき |
古いエフェクトをクリーンアップしてから新しいエフェクトを実行 |
| コンポーネントがアンマウントされたとき | タイマーを確実に停止してメモリリークを防ぐ |
クリーンアップ関数がないと?
isRunningがfalseになってもインターバルが止まらず、バックグラウンドでsetTimeが呼ばれ続けます。これがメモリリークや予期しない再レンダリングの原因になります。
動作フロー
【タイマー開始】
isRunning: false → true
└─ useEffect 実行
└─ setInterval 開始(毎秒 time++)
【タイマー停止】
isRunning: true → false
└─ クリーンアップ関数が呼ばれる
└─ clearInterval でインターバル停止
└─ useEffect 再実行(if(isRunning)がfalseなので何もしない)
【コンポーネント消滅】
アンマウント時
└─ クリーンアップ関数が呼ばれる
└─ clearInterval でインターバル停止
✅️ポイントまとめ
| 項目 | ポイント |
|---|---|
依存配列 [isRunning]
|
isRunningが変わるたびにエフェクトが再実行される |
setInterval |
一定間隔で繰り返し処理を実行する |
関数型更新 (time) => time + 1
|
クロージャによる古い値参照バグを防ぐ |
| クリーンアップ関数 | メモリリーク・多重起動を防ぐ。必ず書く! |
📖おわりに
useEffect + setInterval のタイマー実装は、一見シンプルですが、クリーンアップ処理と関数型更新を正しく使わないと微妙なバグの原因になることがわかりました。
この記事が参考になれば幸いです!