こんにちは、かつコーチです。
前回は「useEffectの依存配列でハマらないためのコツ」を解説しました。
今回は、useEffectとセットでよく登場する useRef を扱います。
「DOM要素を直接触りたいとき」「再レンダリングを起こさずに値を保持したいとき」に欠かせないフックなので、この記事でしっかり使い方を押さえておきましょう。
useRefとは?
再レンダリングを引き起こさない「入れ物」
useRef は、コンポーネントの再レンダリングをまたいで値を保持できる入れ物(オブジェクト)を作るフックです。
useState と似ていますが、決定的な違いがあります。
useState は値を更新すると再レンダリングが走りますが、useRef は値を更新しても再レンダリングが起きません。
import { useRef, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const renderCountRef = useRef(0);
renderCountRef.current = renderCountRef.current + 1;
return (
<div>
<p>カウント: {count}</p>
<p>レンダリング回数: {renderCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
renderCountRef.current はレンダリングのたびに更新されますが、この値自体を更新しても再レンダリングは発生しません。
なぜ再レンダリングされないと嬉しいのか
useState だけで値を管理していると、「表示には関係ないけれど保持しておきたい値」まで再レンダリングの対象になってしまいます。
タイマーIDや直前の入力値など、画面に表示しない値を持たせたいときは useRef の出番です。
useRefの2大用途
用途1:DOM要素への参照を取得する
useRef の代表的な使い道が、DOM要素への直接アクセスです。
import { useRef } from "react";
function TextInputWithFocus() {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>入力欄にフォーカス</button>
</div>
);
}
ref 属性にセットした inputRef は、レンダリング後に inputRef.current へ実際のDOM要素が入ります。
ボタンを押すと inputRef.current.focus() が呼ばれ、入力欄にカーソルが移動します。
TypeScriptで書く場合、useRef<HTMLInputElement>(null) のようにジェネリクス(型を差し込む仕組み)で要素の型を指定しておくと、.current に対する補完や型チェックが効くようになります。
用途2:レンダリングをまたいで値を保持する
もう1つの用途が、setTimeout のIDなど「表示には出さないが覚えておきたい値」の保持です。
import { useRef, useState } from "react";
function DebouncedSearch() {
const [keyword, setKeyword] = useState("");
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
setKeyword(value);
console.log("検索実行:", value);
}, 500);
};
return <input onChange={handleChange} placeholder="検索キーワード" />;
}
timerRef.current にタイマーIDを保持しておくことで、次の入力があったときに前回のタイマーを確実にキャンセルできます。
つまずきやすいポイント:レンダリング中にrefを更新してしまう
かつコーチが実際にハマった例
私が最初に useRef を使ったとき、レンダリング中の処理の中で .current を書き換えてしまい、画面がちらつくような不安定な挙動になったことがあります。
原因は、本来イベントハンドラや useEffect の中で行うべき更新を、コンポーネント本体の中でそのまま実行してしまっていたことでした。
❌ Before:レンダリング処理の中でrefを更新してしまう
import { useRef } from "react";
function LogList({ items }: { items: string[] }) {
const countRef = useRef(0);
// レンダリングのたびにここが実行され、副作用が予測できなくなる
countRef.current = countRef.current + items.length;
return <p>累計アイテム数: {countRef.current}</p>;
}
このコードは、コンポーネントが再レンダリングされるたびに countRef.current を書き換えています。
Reactのレンダリングは「何度呼ばれてもよい純粋な処理」であるべきという前提が崩れ、Strict Modeの二重レンダリングなどで値がずれるバグにつながります。
✅ After:更新はuseEffectやイベントハンドラの中で行う
import { useEffect, useRef } from "react";
function LogList({ items }: { items: string[] }) {
const countRef = useRef(0);
useEffect(() => {
countRef.current = countRef.current + items.length;
}, [items]);
return <p>累計アイテム数: {countRef.current}</p>;
}
useEffect の中で更新するようにすることで、「レンダリングが終わった後に1回だけ実行される」という安全なタイミングに処理を移せます。
レンダリング中はrefを読むだけ、更新はuseEffectかイベントハンドラの中で行う、というルールを徹底すると事故を防げます。
応用:前回の値を覚えておく「usePrevious」パターン
値の変化を検知したいときの定番テクニック
useRef は、「1つ前のレンダリング時点の値」を覚えておくカスタムフックを作るときにもよく使われます。
import { useEffect, useRef } from "react";
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function PriceDisplay({ price }: { price: number }) {
const previousPrice = usePrevious(price);
const isUp = previousPrice !== undefined && price > previousPrice;
return (
<p style={{ color: isUp ? "red" : "blue" }}>
現在価格: {price}円
</p>
);
}
useEffect の中で ref.current を更新しているので、コンポーネント本体で読み取る previousPrice は常に「1つ前のレンダリング時点の値」になります。
このパターンは、価格の上下判定やアニメーションの開始条件など、「変化そのもの」を検知したい場面で応用が効きます。
まとめ
この記事のポイント
useRefは再レンダリングを引き起こさずに値を保持できるフック- 代表的な用途は「DOM要素への参照」と「値の保持」の2つ
- レンダリング中に
.currentを更新するのはNG。更新はuseEffectかイベントハンドラの中で行う - 「前回の値を覚えておく」usePreviousパターンなど、応用範囲が広い
次に読むべき記事
次回は「useContextでProps drillingを解消する」です。
コンポーネントの階層が深くなったときに便利なフックなので、ぜひ続けて読んでみてください。
→ 次の記事:useContextでProps drillingを解消する