はじめに
Reactで定期的に何かを実行したいとき、あなたはどうしていますか?
setInterval(() => {
console.log("毎秒実行");
}, 1000);
このような処理をコンポーネント内に直接書くと、バグやメモリリークの原因になります。
そんなときに使えるのが、useRef
とuseEffect
を組み合わせた副作用の安全な制御方法です。
🎯 本記事では以下をわかりやすく紹介します:
-
useRef
とは何か、なぜ使うのか -
useEffect
との組み合わせ方 - 最小構成の「カウントアップタイマー」実装
useRefとは?
useRef
は主に DOMを参照するためのフックですが、実はそれだけではありません。
const intervalRef = useRef<NodeJS.Timeout | null>(null);
このように書くことで、再レンダリングされても値が保持される「永続的な変数」として使えます。
-
useState
:変更すると再描画が走る -
useRef
:変更しても再描画されない(非表示の箱)
useEffectとは?
useEffect
は「副作用(Effect)」を扱うためのフックです。
副作用とは、例えば以下のような処理です:
- データの取得(fetch)
- イベントリスナーの登録
- タイマー(setInterval, setTimeout)
- 外部ライブラリとの連携
Reactのレンダリングとは直接関係ない処理を、特定のタイミングで実行したいときに使います。
✨ []
は「初期処理」
useEffect(() => {
// この中の処理はコンポーネントが表示されたときに実行されるよ
}, []);
第二引数の「依存配列」が空([]
)だと、初回マウント時のみ実行されるという意味になります。
🧹 return () => { ... }
は「お片付け処理」
さらに、useEffect
の中で return
を書くと、
useEffect(() => {
return () => {
// コンポーネントが画面から消える(アンマウントされる)ときに実行される
};
}, []);
以下のようなタイマーを作っていたとします:
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
これを放っておくと、コンポーネントが画面から消えてもずっと裏で実行され続けてしまうため、メモリリークや予期せぬ動作になります。
なので、「消えるときに止める(お片付け)」が必要になります。
useEffect(() => {
// mount(表示)時の処理:今回はなし
return () => {
// unmount(非表示になるとき)にやりたいことを書く
stop(); // → タイマーを止めるなど
};
}, []);
このように書くことで、
- コンポーネントが**マウントされたとき(表示されたとき)**は何もしない
- コンポーネントが**アンマウントされるとき(画面から消えるとき)**に
stop()
を呼んで、タイマーを止める
という安全な流れが完成します。
「使い終わったあとにちゃんと片付ける」が useEffect
の return
の役割です。
useEffect × useRef 実装例
まずは、setInterval
を安全に使う基本パターンを紹介します。
import { useState, useRef, useEffect } from "react";
export default function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const start = () => {
if (intervalRef.current !== null) return; // 二重防止
intervalRef.current = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
const reset = () => {
stop();
setCount(0);
};
useEffect(() => {
return () => {
stop(); // コンポーネントが消えるときに停止
};
}, []);
return (
<div>
<p>⏱️ Count: {count}</p>
<button onClick={start}>▶️ Start</button>
<button onClick={stop}>⏹ Stop</button>
<button onClick={reset}>🔁 Reset</button>
</div>
);
}
✅ ポイント
-
intervalRef.current
にタイマーIDを保存 -
useEffect
のクリーンアップで自動停止 - 状態と副作用を分離して管理できる
目的 | 適したフック |
---|---|
値を保持したいが再描画は不要 | useRef |
副作用の実行/終了タイミングを制御 | useEffect |
状態管理と画面更新 | useState |
setInterval
や setTimeout
を扱うときは、必ずuseRef
でIDを保持し、useEffect
で開始・終了を管理するのがベストプラクティスです。
おわりに
- Reactでタイマーや自動再生を扱いたい方
-
useRef
の活用方法を知りたい方 - Game of Lifeのような時間ベースのアプリを作りたい方
にとって、useRef
+ useEffect
の組み合わせは非常に強力な武器になります。
ぜひ一度、実際に試してみてください!