useRef — 再レンダーの外で値を持つ
前章の useState は、値を更新すると再レンダーが走るフックでした。
この章で扱う useRef は、値を保持するが、変更しても再レンダーを引き起こさないフックです。
学習者値を覚えておきたいだけなら useState でいいんじゃないの?useRef はいつ使うの?
useRefの仕組み
useRef は .current プロパティを持つオブジェクトを返します。
import { useRef } from 'react';
const countRef = useRef(0);
// countRef → { current: 0 }
countRef.current = 1; // 値は変わるが、再レンダーは起きない.current を直接書き換えられますが、Reactはその変更を監視していないので再レンダーは起きません。
useState とは対照的です。
// useState — 更新すると再レンダーが走る
const [count, setCount] = useState(0);
setCount(1); // → 再レンダー → UIが更新される
// useRef — 更新しても再レンダーは走らない
const countRef = useRef(0);
countRef.current = 1; // → 何も起きない(UIはそのまま)useStateとの使い分け
判断基準は「その値がUIの表示に影響するかどうか」です。
画面に表示する値は useState。値が変わったときに画面を更新してほしいからです。
画面には表示しないが裏側で覚えておきたい値は useRef。不要な再レンダーを避けられます。
letとの違い — なぜuseRefが必要なのか
「再レンダーしないなら普通の変数(let)でいいのでは?」と思うかもしれません。
しかしコンポーネント関数内の let は、再レンダーのたびにリセットされます。
function Counter() {
const [count, setCount] = useState(0);
// NG — 再レンダーのたびに0にリセットされる
let renderCount = 0;
renderCount++;
console.log(renderCount); // 常に1
return <button onClick={() => setCount((c) => c + 1)}>+1</button>;
}useRef なら再レンダーをまたいで値が保持されます。
function Counter() {
const [count, setCount] = useState(0);
// OK — 再レンダーしても値が保持される
const renderCountRef = useRef(0);
renderCountRef.current++;
console.log(renderCountRef.current); // 1, 2, 3... と増えていく
return <button onClick={() => setCount((c) => c + 1)}>+1</button>;
}この違いは「let はJavaScriptの関数スコープで管理される(呼び出しごとにリセット)」のに対し、「useRef はReactのFiberツリーで管理される(再レンダーをまたいで保持)」ことから来ています。stateと同じ保管場所を使いつつ、再レンダーだけは引き起こさないのが useRef です。
典型的な使い方
DOM要素への参照
最もよく使うパターンです。JSXの ref 属性に渡すと、Reactがマウント時にそのDOM要素を .current に入れてくれます。
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>フォーカスする</button>
</>
);
}useRef<HTMLInputElement>(null) の null は初期値です。コンポーネントがマウントされる前はDOM要素がまだ存在しないので、初期値は null にしておきます。マウント後、Reactが inputRef.current に実際のDOM要素を入れてくれます。
DOM参照は、フォーカスの制御、スクロール位置の取得・設定、サードパーティライブラリとの連携(Canvas、動画プレイヤーなど)で使います。
前回の値を覚えておく
function PriceDisplay({ price }: { price: number }) {
const prevPriceRef = useRef(price);
useEffect(() => {
prevPriceRef.current = price; // レンダー後に「今の値」を保存
});
const diff = price - prevPriceRef.current;
return (
<p>
現在 {price}円
{diff !== 0 && (
<span>
({diff > 0 ? '+' : ''}
{diff}円)
</span>
)}
</p>
);
}「前回のpropsやstate」を覚えておきたいケースです。useEffect のクリーンアップではなくコールバック内で保存しているのは、レンダー後に「今の値」を保存して、次のレンダーで「前回の値」として使うためです。
タイマーIDの保持
function StopWatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef<number | null>(null);
const start = () => {
intervalRef.current = window.setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
window.clearInterval(intervalRef.current);
}
};
return (
<div>
<p>{seconds}秒</p>
<button onClick={start}>開始</button>
<button onClick={stop}>停止</button>
</div>
);
}タイマーIDは画面に表示しないので useState ではなく useRef が適切です。
もし useState で持つと、IDが変わるたびに不要な再レンダーが発生します。
useRefの注意点
.currentの変更はレンダーに反映されない
これはメリットであると同時に、間違えやすいポイントでもあります。
function BrokenCounter() {
const countRef = useRef(0);
return (
<div>
{/* この表示は更新されない */}
<p>カウント: {countRef.current}</p>
<button
onClick={() => {
countRef.current++;
}}
>
+1
</button>
</div>
);
}ボタンを押すと countRef.current の値は確かに増えますが、再レンダーが走らないので画面の表示は変わりません。
画面に表示したい値には必ず useState を使ってください。
レンダー中に.currentを読み書きしない(DOM参照以外)
レンダー中(つまりコンポーネント関数の本体)で .current を読み書きすると、Reactの並行レンダリング機能と干渉する可能性があります。読み書きはイベントハンドラや useEffect の中で行うのが安全です。
先ほどの renderCountRef.current++ の例はあくまで動作の違いを説明するためのもので、実務ではエフェクト内で行います。
まとめ
useRef は再レンダーなしに値を保持する手段です。
useState との使い分けの判断基準は「その値がUIに映るか」。DOM参照やタイマーID、前回の値の記憶など、画面に直接表示しないデータに使います。
let はレンダーごとにリセットされますが、useRef はReactのFiberツリーに保管されるため再レンダーをまたいで保持されます。ただし .current を変更しても画面は更新されないので、表示用のデータには使えません。