第7回
useRefの使い方:DOM参照・前回値の保持・レンダリングを起こさない値
ReactのuseRefを解説します。DOM要素への直接参照、前回値の保持、レンダリングをトリガーしない値の管理など、useRefの3つの主要ユースケースを実例付きで学べます。
·13分で読める
たける
検索ボックスのクリアボタンを押したあと、自動でinputにフォーカスを戻したいんですが、どうやってDOMに直接アクセスするんですか?
りこ
useRefでDOM要素への参照を持つ。`ref={inputRef}` とJSXに渡すと、`inputRef.current` にそのDOM要素が入る。あとは `inputRef.current?.focus()` を呼ぶだけ。
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();
}, []); // マウント時に1回だけ
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 = () => videoRef.current?.play();
const pause = () => videoRef.current?.pause();
return (
<div>
<video ref={videoRef} src={src} />
<button onClick={play}>再生</button>
<button onClick={pause}>一時停止</button>
</div>
);
}前回値の保持
useEffect を使って前回のprops/stateを保持できます。
function usePrevious<T>(value: T): T | undefined {
const prevRef = useRef<T | undefined>(undefined);
useEffect(() => {
prevRef.current = value; // レンダリング後に更新
});
return prevRef.current; // 今回のレンダリングでは前回の値を返す
}
function PriceDisplay({ price }: { price: number }) {
const prevPrice = usePrevious(price);
const diff = prevPrice !== undefined ? price - prevPrice : 0;
return (
<div>
<span>¥{price.toLocaleString()}</span>
{diff !== 0 && (
<span style={{ color: diff > 0 ? 'red' : 'blue' }}>
({diff > 0 ? '+' : ''}{diff})
</span>
)}
</div>
);
}レンダリングをトリガーしない値の管理
タイマー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(() => () => stop(), []);
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を子コンポーネントに渡す
カスタムコンポーネントに ref を渡したいときは 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` と `useState` の根本的な違いがいまいちわかってなくて。どちらも値を保持しますよね。
りこ
stateが変わると再レンダリングが起きる。refが変わっても起きない。「画面に反映する値はstate、内部処理だけで使う値はref」。タイマーのID・前回値・DOM参照はrefに入れる。
まとめ
useRefは再レンダリングを起こさずに値を保持する- DOM要素への参照(フォーカス・スクロールなど)に使う
- 前回値の保持や、タイマーIDなどレンダリング不要な値の管理に適している
- UIに反映する値は
useState、内部で使うだけの値はuseRef
次の第8回では、重い計算結果をメモ化して再計算を防ぐ useMemo を学びます。