💨
(React) useRefについてまとめてみました。
useRef
これまで useRef
は、レンダリング結果から取得した DOM ノードにアクセスするために使われることが多かったです。
例えば、以下のように input 要素にフォーカスを当てるケースが代表的でしょう。
import { useRef, useEffect } from "react";
function MyComponent() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
しかし useRef
はもっと奥深いところがありました。
レンダー間で値を保持するインスタンス変数としての性質を活かすことで、
ゲームループやタイマー管理、外部ライブラリのインスタンス保持、
さらに「前回の state の記憶」など、さまざまな場面で活躍してくれました。
今日は、その「インスタンス変数」としての性質を活かすuseRef
についてまとめてみます。
レンダー間で値を保持するインスタンス変数
-
useRef(initialValue)
は{ current: 初期値 }
のオブジェクトを返します。
初期値はマウント時に一度だけ適用され、再レンダーでは無視されます。 -
.current
を更新しても再レンダーは発生しません。
※「強制的に再レンダーが必要な場合」は、useState
やforceUpdate
などで明示的に制御する必要があります。 - 再レンダー不要な内部変数を安全に保持できるため、タイマー管理や一意ID発行などに最適です。
例 1. 定期的にアイテムを生成する
設定時間が経過すると新しいアイテムを生成するロジックです。
const spawnTimer = useRef(0); // 初期値はマウント時のみ
const nextId = useRef(0);
const update = useCallback((dt: number) => {
// 経過時間を加算
spawnTimer.current += dt;
// spawnIntervalに到達したらアイテムを生成
if (spawnTimer.current >= spawnInterval) {
// タイマーをリセット(再レンダーなし)
spawnTimer.current = 0;
// 新アイテム生成(ID発行も内部refで管理)
const newItem = {
id: nextId.current++, // IDが変わっても再レンダーしない
x: Math.random() * 400
y: 40,
speed: Math.random() * (5 - 2) + 2,
size: 20
};
setItems(prevItems => [...prevItems, newItem]); // この部分でのみレンダーが発生
}
}, [setItems, spawnInterval]);
このコードは、次のようにイメージできます。
- スピード違反取り締まりタイマー(spawnTimer)
- 毎フレーム「前フレームからの経過時間(dt)」を足し合わせ、一定時間ごとに処理を実行
- 値を更新してもレンダーは起こらず、内部だけで時間を管理
- 番号発行機(nextId)
- アイテムごとに一意のIDを付与
- こちらも内部カウンターとして保管し、再レンダーを防止
- アイテム生成の仕組み
- タイマーがしきい値を超えたらspawnTimerをリセット
- nextIdの番号を使って新しいアイテムを作成
- アイテム配列の更新(setItems)で初めて画面がリレンダー
こうすることで、**「内部で値を保持しつつ必要なときだけレンダー」**が実現できます。
例 2. IntervalIDを固定
- IntervalRefを使ってIDを取得
- useStateを使って1秒ごとに数字を増加させる
import React, { useRef, useState } from "react";
import "./styles.css";
export default function App() {
// インターバルIDを保持するRef
const intervalRef = useRef(null);
// カウンターを保持するState
const [counter, setCounter] = useState(0);
const handleClick = () => {
// 既存のタイマーがあればクリア
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
}
// 新しいインターバルを開始してIDをRefに保存
intervalRef.current = window.setInterval(() => {
setCounter((c) => c + 1);
}, 1000);
};
const closeClick = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null; // 停止後はnullにリセット
}
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={handleClick}>カウンターをスタート</button>
<button onClick={closeClick}>カウンターをストップ</button>
<p>Interval ID (Ref): {intervalRef.current ?? "—"}</p>
<p>カウント数 (State): {counter}</p>
</div>
);
}
ボタンとクリックして試してみましょう
まとめ:
-
useRef
は「インスタンス変数」として、再レンダーが不要な値を保持する場面で大活躍 - 初期値はマウント時に一度だけ設定され、以降の再レンダーでは無視される。
- .current 更新はレンダーを起こさないため、タイマーIDや内部カウンタなどの管理に最適。
- 外部ライブラリのインスタンスやタイマーを使う場合は、useEffect のクリーンアップ(destroy()/clearInterval())を忘れずに。
-
状態管理(state) と 内部変数(ref) を使い分けることで、余計な再レンダーを防ぎつつ、
シンプルでパフォーマンスの高い実装が可能になります。
Discussion