背景
これまでReactで使うフックは useState や useEffect が多く、useRef に関しては「DOM にアクセスするためのフック」くらいの理解で止まってしまい、使いどころが分からずモヤモヤしていました。
- 「値を保持できるのにレンダーが走らないってどういうこと?」
- 「それなら全部 useRef でいいんじゃないの?」
- 「DOM参照以外に何に使うの?」
といった疑問が常に頭の中にあり、正直 useRef は「知っているけど使いこなせないフック」でした。
しかし実際のプロジェクトでは、useRef を正しく理解して使うことで 「不要な再レンダーを避けられる」、「前回の値を記録できる」、「タイマーや外部リソースを安定して扱える」 といった便利さを実感しました。
useRefとは
useRefの基本
useRef の概要は、「再レンダーされても値を保持し続けるための箱」 です。
この箱は .current というプロパティを持ち、そこに任意の値を格納できます。
const ref = useRef(0);
console.log(ref.current); // => 0
ref.current = 100;
console.log(ref.current); // => 100
ここで重要なのは、ref.current の値を更新してもコンポーネントは再レンダーされないという点です。
useStateとの違い
よく比較されるのが useState で、両方とも「値を保持する」役割を持ちますが、挙動に違いがあります。
| フック | 値を更新したときの挙動 | 主な用途 |
|---|---|---|
| useState | 値を更新すると再レンダーが走る | UI に反映したい値の管理 |
| useRef | 値を更新しても再レンダーは発生しない | 再レンダー不要な値や DOM 参照の保持 |
直感的なイメージ
-
useState→ 「値の変化を画面に反映させたいとき」(例: カウンターの数値) -
useRef→ 「値の変化を画面に表示する必要はないとき」(例: setInterval の ID、前回の入力値、スクロール位置など)
DOM参照にも使える
useRef のもうひとつの代表的な用途は DOM要素へのアクセス です。
React では直接 DOM を触ることを避ける設計が基本ですが、どうしても必要な場面(フォーカス操作など)があります。
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>フォーカス</button>
</>
);
ref 属性に useRef を渡すと、.current に実際の DOM 要素が格納され、直接操作できます。
3. 基本的な使い方
① DOM要素へのアクセス
もっとも分かりやすい使い方は DOM操作 です。
例えば、ボタンを押したら入力欄にフォーカスするケース。
import { useRef } from "react";
export default function App() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} placeholder="名前を入力" />
<button onClick={() => inputRef.current?.focus()}>
フォーカスする
</button>
</>
);
}
ref を input に渡すと、inputRef.current に DOM 要素が入り、focus() を呼び出せます。
② 値を保持する(再レンダー不要)
useRef は コンポーネントが再レンダーされても値を保持します。
例えば「前回の値を記録する」ケース。
import { useState, useEffect, useRef } from "react";
export default function App() {
const [count, setCount] = useState(0);
const prevCount = useRef<number>();
useEffect(() => {
prevCount.current = count;
}, [count]);
return (
<div>
<p>現在: {count}</p>
<p>前回: {prevCount.current}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
prevCount.current に前回の値が保存され、再レンダー後も消えません。
useState だと再レンダーを引き起こしてしまいますが、useRef なら裏でこっそり保持できます。
③ setInterval の ID 管理
useRef は タイマーや外部リソースの ID を保持するのにも便利です。
import { useRef } from "react";
export default function Timer() {
const timerId = useRef<number | null>(null);
const start = () => {
if (!timerId.current) {
timerId.current = window.setInterval(() => {
console.log("tick");
}, 1000);
}
};
const stop = () => {
if (timerId.current) {
clearInterval(timerId.current);
timerId.current = null;
}
};
return (
<>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</>
);
}
timerId を useState で管理すると不要な再レンダーが発生しますが、useRef なら再レンダーせずに保持できます。
実務でよくある使いどころ
useRef は公式ドキュメントでは「DOMアクセス」によく使われると説明されていますが、実際のプロジェクトではそれ以上に便利なケースが多いです。ここでは、自分が実務で遭遇したりよく見かけるパターンをまとめます。
① フォーム入力でのフォーカス制御
バリデーションエラーが出たときに、最初のエラー項目へ自動でフォーカスするようなケースです。
if (!formData.name) {
inputRef.current?.focus();
}
ユーザー体験を向上させるために 「エラー箇所へスクロール or フォーカス」 する処理はよくあります。
② スクロール位置の保持
SPAでは、ページ遷移後に戻ったときに前回のスクロール位置を復元したい場面があります。
const scrollY = useRef(0);
useEffect(() => {
const handleScroll = () => {
scrollY.current = window.scrollY;
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
再レンダーに影響を与えないので、スクロール位置や座標系の保存にぴったりです。
③ 非同期処理のキャンセルフラグ
useEffect 内で非同期処理を行うとき、アンマウント後に setState を呼んでエラーになるのを避けるために useRef でフラグを持たせる方法があります。
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
fetch("/api/data").then(res => {
if (isMounted.current) {
// 安全に更新できる
setData(res);
}
});
}, []);
実務では「非同期処理とコンポーネントのライフサイクルを安全に扱う」ための小技としてよく使います。
④ 前回の値を比較したいとき
例えば 検索フォームで「前回の検索条件」と「今回の検索条件」を比較して差分だけ API に投げるような場面です。
const prevQuery = useRef<string>("");
useEffect(() => {
if (prevQuery.current !== query) {
fetchData(query);
prevQuery.current = query;
}
}, [query]);
「前回の値を保持して差分を見る」というパターンは地味によく出てきます。
注意点とまとめ
注意点・落とし穴
実務で useRef を使っていて、最初にハマりやすいポイントを挙げます。
-
値を更新しても画面に反映されない
-
useRefの更新は再レンダーを引き起こさないので、UI に表示したい値は必ずuseStateを使うべきです。 - 「
ref.currentを変えたのに表示が更新されない」というのは典型的な勘違いポイント。
-
-
初期値は
nullになることが多い- DOM参照に使う場合、最初のレンダー時はまだ要素が存在しないので
ref.currentはnull。 - TypeScript を使う場合は
useRef<HTMLInputElement>(null)のように型にnullを許容する必要があります。
- DOM参照に使う場合、最初のレンダー時はまだ要素が存在しないので
-
やりすぎ注意
- なんでもかんでも
useRefにすると「再レンダーされないせいでUIが古い値を持ち続ける」ことがあります。 - 「画面に出す値は state、裏で持っておきたい値は ref」と意識的に使い分けが大事です。
- なんでもかんでも
まとめ
-
useRefは 「再レンダーを引き起こさずに値を保持する箱」 -
主な用途は
- DOM要素へのアクセス(フォーカス、スクロールなど)
- 再レンダー不要な値の保持(前回の値、setIntervalのIDなど)
-
useStateとの違いは 「UIに反映したいなら state、裏で保持したいなら ref」 -
注意点は「画面に反映されない」「初期値は null」「乱用しない」