フロントエンドの現場で、`useEffect` と `setTimeout` や `setInterval` を組み合わせて「何かがうまくいかない」と頭を抱えた経験はないだろうか?
「タイマーが二重に走る」「コンポーネントをアンマウントしたはずなのにメモリリークの警告が出る」「Stateが古い値のまま更新されない」。これらはすべて、ReactのレンダリングライフサイクルとブラウザのタイマーAPIの「非同期なすれ違い」が原因だ。
今日は、中級エンジニアなら絶対に押さえておくべき、タイマー処理の「正しい作法」について、現場の泥臭い知見を交えて解説する。
—
1. なぜ `useEffect` でタイマーを扱うと事故るのか
Reactの `useEffect` は、あくまで「同期的なレンダリング」が終わったあとに動く「副作用」だ。
一方で、`setInterval` などのタイマーは、ブラウザのタイマー管理スレッドで独立して動いている。React側でコンポーネントが破棄(アンマウント)されても、ブラウザが保持しているタイマーは「コードで明示的に停止させない限り」止まらない。
これが、いわゆる「ゾンビタイマー」だ。アンマウントされたコンポーネントが、存在しないStateを更新しようとしてエラーを吐く、あるいはバックグラウンドで無駄なCPU負荷をかけ続ける。これを防ぐのが、`useEffect` の戻り値である「クリーンアップ関数」の役割だ。
—
2. 現場で使える「クリーンアップ関数」の実装パターン
最も重要なのは、`useEffect` の中でタイマーをセットしたら、必ずその直後にクリーンアップ関数で解除するという鉄則だ。
以下は、コンポーネントが消えた瞬間にタイマーを確実に殺すためのベストプラクティスだ。
import { useEffect } from ‘react’;
const TimerComponent = () => {
useEffect(() => {
// 1. タイマーIDを保持する
const timerId = setInterval(() => {
console.log(‘タイマーが発火しました’);
}, 1000);
// 2. クリーンアップ関数を返す
// コンポーネントがアンマウントされる直前、
// または次のeffectが実行される直前に必ず呼び出される
return () => {
clearInterval(timerId);
console.log(‘タイマーをクリーンアップしました’);
};
}, []); // 依存配列が空なら、マウント時とアンマウント時の1回ずつのみ実行
return
;
};
—
3. 「Stateが古い」問題には `useRef` を添える
タイマーのコールバック内でStateを参照しようとしたとき、依存配列にそのStateを含めると、Stateが更新されるたびに `useEffect` が再実行され、タイマーが破棄・再生成を繰り返す。これはパフォーマンス的にもロジック的にも最悪だ。
ここで「伝説のアーキテクト」からのアドバイス。「最新の値を知る必要があるが、effectを再発火させたくない」なら `useRef` を使え。
import { useState, useEffect, useRef } from ‘react’;
const Counter = () => {
const [count, setCount] = useState(0);
// useRefはレンダリングに影響を与えず、値を保持し続けるための箱
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 最新の値をrefに同期
}, [count]);
useEffect(() => {
const timerId = setInterval(() => {
// タイマー内ではref経由で最新の値にアクセスする
console.log(`現在のカウント: ${countRef.current}`);
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(timerId);
}, []); // タイマーは一度しかセットしない
return
{count}
;
};
このように `useRef` を噛ませることで、Reactのレンダリングサイクルからタイマーを切り離すことができる。これが、スケールするReactアプリケーションを作るための「疎結合」な思考だ。
—
4. 最後に:なぜ「泥臭い」配慮が必要なのか
Reactは宣言的で美しいライブラリだが、ブラウザという「命令的で泥臭い環境」の上で動いている。
公式ドキュメントに書いてあることだけを鵜呑みにするのではなく、「ブラウザのメモリ上で今何が起きているか」「タイマーIDは誰が管理しているのか」を想像してほしい。
- クリーンアップ関数は「保険」ではなく「義務」である。
- 依存配列を汚す前に `useRef` で逃げ道を作れないか考える。
これらを守るだけで、君たちの書くコードは一気にプロダクションレベルの堅牢さを手に入れるはずだ。もし明日、チームメイトがタイマーの管理で悩んでいたら、この「クリーンアップの掟」を教えてやってくれ。それが技術の継承というものだ。
さて、次は `AbortController` を使った非同期処理のキャンセルについても話したいところだが、それはまた別の機会にしよう。現場からは以上だ。

コメント