【テクニカル・上級編】 setTimeout/setIntervalの管理とクリーンアップ – React実践ガイド

useEffectでタイマーを飼い慣らす:Reactにおける非同期ライフサイクルの深淵

Reactの`useEffect`とタイマー処理(`setTimeout`/`setInterval`)の組み合わせは、一見すると初学者の登竜門のように思える。しかし、現場で「なんとなく動いている」コードを放置すれば、それはやがてメモリリーク、重複実行、そしてUIの不整合という名の技術的負債として牙を剥く。

上級エンジニアである君たちには、単に「`clearInterval`を呼べばいい」という表面的な知識のその先、ReactのレンダリングサイクルとJavaScriptのイベントループが交差する「境界線」での制御について語りたい。

なぜ、タイマーのクリーンアップは「義務」なのか

Reactのコンポーネントは、ブラウザのタイムライン上で常に生成と破棄を繰り返すエフェメラル(一時的)な存在だ。`setInterval`を`useEffect`内で開始した際、クリーンアップ関数を定義し忘れると何が起きるか。

コンポーネントがアンマウントされても、JavaScriptのスコープ外でタイマーは生き続け、メモリを食いつぶす。さらに、ReactのStrict Mode(開発環境)ではエフェクトが二重に実行されるため、クリーンアップをサボるとタイマーが二重、四重に重なり、UIの更新処理がブラウザのメインスレッドを窒息させることになる。

堅牢なタイマー実装のベストプラクティス

多くのエンジニアが陥る罠は、依存配列にタイマーIDや関数を安易に入れてしまうことだ。まずは、最もクリーンで再利用性の高い実装パターンを見てほしい。

import { useEffect, useRef } from ‘react’;

/

  • 堅牢なインターバル管理用カスタムフック
  • 依存配列の変更によるタイマーの再生成を最小限に抑える

/
function useInterval(callback, delay) {
// コールバックをrefに保持することで、依存配列にcallbackを入れずに済む
// これにより、callback内のstate更新による意図しないタイマー再起動を防ぐ
const savedCallback = useRef(callback);

useEffect(() => {
savedCallback.current = callback;
}, [callback]);

useEffect(() => {
if (delay === null) return;

const id = setInterval(() => {
savedCallback.current();
}, delay);

// 確実にタイマーを破棄し、メモリリークを塞ぐ
return () => clearInterval(id);
}, [delay]);
}

なぜ `useRef` を介在させるのか?

この設計には二つの高度な意図がある。

1. レンダリングサイクルの分離: `callback`を`useEffect`の依存配列に入れてしまうと、親コンポーネントがレンダリングされるたびにタイマーがクリアされ、再生成される。これはパフォーマンス的に極めて非効率だ。`useRef`を挟むことで、`setInterval`自体は`delay`が変わらない限り生き続け、中身のロジックだけが最新の状態に差し替えられる。
2. 非同期の競合回避: Reactのレンダーパスとタイマーの実行タイミングがズレた際、古い状態(Stale State)を参照してしまうバグを未然に防げる。

Reactの内部挙動を意識した「泥臭い」最適化

大規模なアプリケーションでは、タイマーが「いつ実行されるべきか」をブラウザエンジンに委ねるだけでは不十分なケースがある。

1. ページ非表示時の制御

ユーザーがタブを切り替えた際、`setInterval`がバックグラウンドで動き続けるのはリソースの無駄だ。`document.visibilityState`を監視し、非表示時はタイマーを一時停止させるロジックを組み込むのが、真のプロフェッショナルの仕事だ。

2. レンダリング負荷の軽減

タイマー内で重い状態更新を行う場合、`requestAnimationFrame`との併用を検討すべきだ。Reactの更新はあくまで「UIの同期」だが、タイマーは「時間の管理」である。両者を同期させるために、自前で`requestAnimationFrame`をラップしたタイマーを実装することで、ブラウザの描画周期に合わせた極めてスムーズなUI更新が可能になる。

結論:副作用とは「制御可能な混沌」である

`useEffect`におけるタイマー管理の本質は、Reactの宣言的な世界観と、JavaScriptの命令的な非同期処理をいかにして「綺麗に繋ぎ合わせるか」にある。

  • クリーンアップ関数は宗教的義務として書け
  • 依存配列を汚すな(`useRef`で隠蔽せよ)
  • ブラウザのアイドル時間や表示状態を常に意識せよ

この三つを遵守するだけで、君たちが書くコードの「現場での生存率」は劇的に向上するはずだ。技術は細部に宿る。ライブラリの裏側にあるブラウザの鼓動を感じながら、次の一行を書いてほしい。

コメント

タイトルとURLをコピーしました