第7回
useRefの使い方:DOM参照・前回値の保持・レンダリングを起こさない値
ReactのuseRefを解説します。DOM要素への直接参照、前回値の保持、レンダリングをトリガーしない値の管理など、useRefの3つの主要ユースケースを実例付きで学べます。
制作・運営:React Guide 運営者
useRefとは
useRef は 再レンダリングを引き起こさずに値を保持する Hookです。
const ref = useRef(initialValue);
// ref.current に値が入るuseState との違いは、ref.current を変更してもコンポーネントが再レンダリングされない点です。
useRefの主なユースケースは3つあります:
- DOM要素への参照(フォーカス・スクロール・アニメーション)
- 前回値の保持(変化を検知する)
- レンダリングをトリガーしない値の管理(インターバルIDなど)
DOM要素への参照
ref をJSX要素に渡すと、ref.current にそのDOM要素が入ります。
function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus(); // DOMの focus() を直接呼ぶ
};
return (
<div>
<input ref={inputRef} type="text" placeholder="検索..." />
<button onClick={focusInput}>フォーカス</button>
</div>
);
}マウント時に自動フォーカス
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []); // 接続時に開始(開発中のStrictModeでは再接続も確認される)
return <input ref={inputRef} type="text" />;
}スクロール制御
function ChatWindow() {
const bottomRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
// 新しいメッセージが来たら一番下にスクロール
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<div style={{ height: 400, overflowY: 'scroll' }}>
{messages.map((msg, i) => <div key={i}>{msg}</div>)}
<div ref={bottomRef} /> {/* スクロール先のアンカー */}
</div>
);
}動画・音声のコントロール
function VideoPlayer({ src }: { src: string }) {
const videoRef = useRef<HTMLVideoElement>(null);
const play = () => { void videoRef.current?.play().catch(console.error); };
const pause = () => videoRef.current?.pause();
return (
<div>
<video ref={videoRef} src={src} />
<button onClick={play}>再生</button>
<button onClick={pause}>一時停止</button>
</div>
);
}表示に使う「前回値」はstateで扱う
refは変更してもレンダーを起こしません。レンダー中にref.currentを読み、それを画面表示の判断に使う汎用usePreviousは、並行レンダリングなどで扱いが難しくなるため、ここでは使いません。
価格の増減を表示したい場合は、価格が変わったことを条件にstateを更新します。次はコンポーネントの定義です。useStateのimportが必要です。
function PriceDisplay({ price }: { price: number }) {
const [lastPrice, setLastPrice] = useState(price)
const [difference, setDifference] = useState(0)
if (price !== lastPrice) {
setLastPrice(price)
setDifference(price - lastPrice)
}
return <p>価格:{price}円(前回との差:{difference}円)</p>
}これは同じコンポーネントのstateだけを、priceが変わったときに限って調整する例です。無条件にsetStateするとループします。外部への送信やDOM操作をレンダーに入れてよいという意味ではありません。
レンダリングをトリガーしない値の管理
タイマーIDや外部ライブラリのインスタンスなど、レンダリングに影響しない値を保持するのに適しています。
function Stopwatch() {
const [elapsed, setElapsed] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
if (intervalRef.current !== null) return;
intervalRef.current = setInterval(() => {
setElapsed((prev) => prev + 100);
}, 100);
};
const stop = () => {
if (intervalRef.current === null) return;
clearInterval(intervalRef.current);
intervalRef.current = null;
};
const reset = () => {
stop();
setElapsed(0);
};
// クリーンアップ
useEffect(() => () => {
if (intervalRef.current !== null) clearInterval(intervalRef.current);
}, []);
return (
<div>
<p>{(elapsed / 1000).toFixed(1)}秒</p>
<button onClick={start}>スタート</button>
<button onClick={stop}>ストップ</button>
<button onClick={reset}>リセット</button>
</div>
);
}インターバルIDを useState で管理すると、IDが変わるたびに再レンダリングが起きてしまいます。useRef を使うことで無駄な再レンダリングを防げます。
useStateとuseRefの使い分け
| 目的 | 推奨 |
|---|---|
| UIに表示する値 | useState |
| 変更してもUIが変わらない値 | useRef |
| DOM要素への参照 | useRef |
| 前回値の記憶 | useRef |
// ❌ レンダリング不要な値にuseStateを使う(無駄な再レンダリング)
const [timerId, setTimerId] = useState<number | null>(null);
// ✅ useRefを使う
const timerIdRef = useRef<number | null>(null);forwardRefでrefを子コンポーネントに渡す
React 19では関数コンポーネントのpropsとしてrefを受け取れます。次はReact 18との互換を保つforwardRefの例です。
import { forwardRef } from 'react';
type InputProps = {
label: string;
placeholder?: string;
};
// forwardRef でrefを受け取れるようにする
const LabeledInput = forwardRef<HTMLInputElement, InputProps>(
({ label, placeholder }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
</div>
)
);
// 使い方
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<LabeledInput ref={inputRef} label="名前" placeholder="入力してください" />
<button onClick={() => inputRef.current?.focus()}>フォーカス</button>
</div>
);
}まとめ
useRefは再レンダリングを起こさずに値を保持する- DOM要素への参照(フォーカス・スクロールなど)に使う
- 前回値の保持や、タイマーIDなどレンダリング不要な値の管理に適している
- UIに反映する値は
useState、内部で使うだけの値はuseRef
次の第8回では、重い計算結果をメモ化して再計算を防ぐ useMemo を学びます。
確認してみる
スタートを連打してもタイマーが増殖しないか、画面を外した後に止まるか確認する。この加算式は正確な時計ではなく通知回数の学習例。
同じテーマの教材
useState・useRef・useReducerの型定義:TypeScriptで正しく型をつける
TypeScriptでuseState・useRef・useReducerに型をつける方法を解説。型推論が効くケースと明示が必要なケース、useRefのnull初期値問題まで実例付きで説明します。
【解決】useEffectでasync/awaitを使う正しい方法と注意点
useEffectに直接asyncを付けてはいけない理由と、正しいasync/awaitの書き方を解説。内部関数として定義する方法・クリーンアップとの組み合わせ・競合状態(race condition)の防ぎ方まで実例付きで説明します。
Hooksのルールとよくある間違い:シリーズ総まとめ
ReactのHooksが守るべきルールを解説します。条件分岐・ループ内での呼び出し禁止の理由、ESLint設定、無限ループや古い値参照などよくある間違いとその対処法をシリーズの集大成としてまとめます。