React で「入力欄にフォーカスを当てたい」「再描画とは関係なく値を覚えておきたい」というとき、useState ではうまくいかないことがあります。そんな場面で使うのが useRef フックです。useRef は「DOM 要素を直接触る」ための入り口であり、同時に「再レンダリングを起こさずに値を保持する箱」でもあります。この記事では、useRef の基本、DOM 操作での使い方、useState との違い、そして値の保持としての使い方までを、初心者向けに具体的なコードで解説します。
目次
useRef の基本と ref オブジェクト
useRef(初期値) を呼ぶと、{ current: 初期値 } という形のオブジェクト(ref オブジェクト)が返ります。値には ref.current でアクセスし、ref.current = ... で書き換えます。最大の特徴は、current を書き換えても再レンダリングが起きないことです。
import { useRef } from 'react';
function Example() {
// { current: 0 } という箱が作られる
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1; // 書き換えても再レンダリングは起きない
console.log(countRef.current); // クリックのたびに 1, 2, 3... と増える
};
return <button onClick={handleClick}>カウント</button>;
}
ボタンをクリックすると countRef.current は増えていきますが、画面の表示は変わりません。useRef が保持する値は再レンダリングをまたいで保たれ、かつ変更しても再描画のきっかけにはならないのです。この性質が、DOM 操作と「表示に出さない値の保持」という2つの用途につながります。
DOM 要素を操作する(もっとも多い用途)
useRef のいちばん代表的な使い方は、DOM 要素への参照を持つことです。JSX の要素に ref 属性を渡すと、React がマウント後にその DOM 要素を ref.current に入れてくれます。これを使えば、入力欄へのフォーカスやスクロール位置の操作など、React の外側の DOM 操作ができます。
import { useRef } from 'react';
function SearchBox() {
// 初期値は null。要素の型を指定しておく
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
// マウント後は current に input 要素が入っている
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="検索..." />
<button onClick={focusInput}>入力欄にフォーカス</button>
</div>
);
}
<input ref={inputRef} /> と書くと、React がその <input> の DOM 要素を inputRef.current にセットします。ボタンを押すと inputRef.current.focus() が呼ばれ、入力欄にカーソルが移動します。初期表示の時点では要素がまだ存在しないため current は null です。そのため ?.(オプショナルチェーン)を付けて安全に呼び出しています。TypeScript では useRef<HTMLInputElement>(null) のように要素の型を指定しておくと、focus() などのメソッドを補完・型チェックできます。
useState との違いと使い分け
useRef と useState はどちらも「値を保持する」点で似ていますが、決定的な違いは変更したときに再レンダリングが起きるかどうかです。
| 観点 | useState | useRef |
|---|---|---|
| 値の変更で再レンダリング | 起きる | 起きない |
| 値の読み書き | state / setState() | ref.current を直接読み書き |
| 画面表示への反映 | 反映される | そのままでは反映されない |
| 主な用途 | 表示に使う状態 | DOM 参照・表示しない値の保持 |
判断の基準はシンプルです。その値が変わったときに画面を更新したいなら useState、更新する必要がないなら useRef を選びます。たとえば「入力された文字を画面に表示する」なら useState、「直前のタイマー ID を覚えておく」「フォーカス対象の要素を参照する」なら useRef です。逆に、表示に使う値を useRef で持つと、値は変わっているのに画面が古いまま、という不具合になります。
再レンダリングされても消えない値を持つ
DOM 参照以外に、useRef は「再レンダリングをまたいで値を覚えておきたいが、表示には使わない」ケースでも役立ちます。よくあるのが、setInterval や setTimeout が返すタイマー ID の保持です。ID は表示に使いませんが、後で clearInterval するために覚えておく必要があります。
import { useRef, useState } from 'react';
function Timer() {
const [count, setCount] = useState(0);
// タイマー ID を保持する箱(表示には使わない)
const timerRef = useRef<number | null>(null);
const start = () => {
if (timerRef.current !== null) return; // 二重起動を防ぐ
timerRef.current = window.setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};
const stop = () => {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
return (
<div>
<p>経過: {count} 秒</p>
<button onClick={start}>開始</button>
<button onClick={stop}>停止</button>
</div>
);
}
ここでは表示する「経過秒数」は useState で管理し、内部的に必要なだけの「タイマー ID」は useRef で保持しています。もし ID を通常の変数で持つと、再レンダリングのたびに初期化されて clearInterval できなくなります。useRef なら値が保たれるので、「開始したタイマーを後から止める」といった処理が正しく書けます。
レンダリング中に current を読み書きしないよう注意
useRef でつまずきやすいのが、ref オブジェクトを JSX の中(レンダリング中)で直接読み書きしてしまうケースです。DOM 参照の current は初期レンダリング時点では null で、値がセットされるのはマウント後です。そのためレンダリング中に current を前提にした処理を書くと、初回に null エラーが起きたり、値が古いままになったりします。
ref.current の読み書きは、イベントハンドラの中や useEffect の中——つまりレンダリングが終わったあとのタイミングで行うのが原則です。また、current を変えても画面は更新されないため、「値を変えたら表示も変えたい」という用途なら useState を使う、という切り分けを忘れないようにしましょう。
まとめ
useRef は { current: 値 } という箱を返すフックで、2つの使い道があります。1つは ref 属性と組み合わせて DOM 要素を直接操作する使い方(フォーカス、スクロールなど)、もう1つは再レンダリングをまたいで値を保持する使い方(タイマー ID など表示しない値)です。共通する性質は「current を書き換えても再レンダリングが起きない」こと。だからこそ、画面に反映したい値は useState、反映しなくてよい値や DOM 参照は useRef、という使い分けになります。current の読み書きはイベントハンドラや useEffect の中で行い、レンダリング中には触らないことを意識すれば、useRef を安全に活用できます。