4.5 Refs — エスケープハッチ
React の基本は「state と props を変えれば、React が DOM を同期してくれる」ことでした。でも、たまに React に面倒を見てもらわずに、自分で直接操作したい 場面に出会います。そのための脱出口(escape hatch)が Ref です。公式ドキュメントも Ref を「エスケープハッチ」と呼んでいます。名前の通り、例外的な使い方のための道具だと思ってください。
Ref の 2 つの用途
Ref の使い道は、次の 2 つに整理できます。
- DOM 要素への参照: 入力欄に focus を当てる、要素のサイズを測る、サードパーティライブラリに DOM を渡す、など。
- レンダーに影響しない可変値の保管場所: setInterval の ID、前回の値、外部ライブラリのインスタンスなど。state に入れると毎回再レンダーされてしまう値を、こっそり保持しておく場所です。
共通しているのは、Ref の変更では再レンダーが起きない ことです。再レンダーさせたい値は state、させたくない値は Ref、と覚えるとすっきりします。
図 4.5.1 Ref と state の住み分け
useRef の基本形
// react.dev/reference/react/useRef より
import { useRef } from "react";
const ref = useRef(initialValue);
// ref.current が実体戻り値は { current: initialValue } 相当のオブジェクトで、毎レンダー同じ参照 が返ります(ここが useState と違うポイント)。ref.current は直接代入で変更できますが、React はそれを検知しません。
用途 1: DOM を参照する
import { useRef } from "react";
export function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>フォーカス</button>
</>
);
}JSX の ref={inputRef} と書くと、コミット時 に inputRef.current に DOM ノードが入ります。レンダー中には null の可能性があるので、イベントハンドラや useEffect の中で触る のが原則です。
触って確かめる — focus と ref のライフサイクル
下のデモでは、focus() / select() を実際に React の ref 経由で呼び出すのと同じ感覚を体験できます。「アンマウント」ボタンで input を外すと、ref の detach(React 19 なら ref コールバックの cleanup が呼ばれるタイミング)がログに出ます。
(ボタンを押すと ref の付け外しと操作が記録されます)
input がマウントされると attach、アンマウント時に detach のログが出ます。 React 19 の ref コールバックで返した cleanup 関数は、この detach のタイミングで呼ばれます。 focus / select はイベントハンドラの中から ref.current を触る例です。
よくある用途
inputRef.current.focus()— 入力欄にフォーカスinputRef.current.select()— テキスト全選択videoRef.current.play()/.pause()— 動画制御el.getBoundingClientRect()— 位置・サイズ計測- Chart.js / Leaflet / Three.js など DOM に描画するライブラリに要素を渡す
やってはいけないこと
React が管理している DOM を勝手に書き換える のは NG です。React は次のレンダーで自分が把握している構造を前提に diff を取るので、外部から子要素を消したり追加したりすると破綻してしまいます。
// NG: React が children を管理している要素を直接消す
divRef.current?.removeChild(...);自分が作った要素(React が children を管理していない領域)なら触って構いません。
用途 2: レンダー外の可変値
タイマー ID や、前回の props の値など、「表示には使わないけれど次のレンダーまで保持したい」値を持つ場所として使います。
import { useRef, useState, useEffect } from "react";
export function Stopwatch() {
const [now, setNow] = useState<number | null>(null);
const intervalRef = useRef<number | null>(null);
function start() {
setNow(Date.now());
intervalRef.current = window.setInterval(() => {
setNow(Date.now());
}, 100);
}
function stop() {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
useEffect(() => stop, []); // アンマウント時に止める
return (
<>
<button onClick={start}>開始</button>
<button onClick={stop}>停止</button>
<p>{now}</p>
</>
);
}intervalRef を state にしてしまうと、ID が入った瞬間に再レンダーが走って無駄です。Ref であれば再レンダーは起きず、静かに値を持てます。
禁じ手: レンダー中に ref を読み書きしない
// NG
function Bad() {
countRef.current++; // レンダー中にミューテート
return <p>{countRef.current}</p>; // レンダー中に読む
}レンダー関数は 純粋(同じ入力 → 同じ出力)であるべきです。Ref をレンダー中に書き換えると、Strict Mode の二重実行や Concurrent レンダリングで挙動が不定になります。Ref の読み書きは イベントハンドラ / useEffect / useLayoutEffect から行う、と覚えてください。
React 19: ref を普通の prop として受け取る
React 18 系で関数コンポーネントに ref を渡すには、forwardRef で包む必要があります。
// forwardRef を使う書き方(React 19 でも動くが非推奨の方向)
import { forwardRef } from "react";
const MyInput = forwardRef<HTMLInputElement, { label: string }>(
function MyInput({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
);React 19 では、ref を普通の prop として受け取って OK になっています。
// React 19 の書き方
function MyInput({ label, ref }: { label: string; ref?: React.Ref<HTMLInputElement> }) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
// 使う側は変わらない
<MyInput ref={myRef} label="名前" />;公式ブログによれば、forwardRef は将来のバージョンで非推奨予定です。codemod で自動変換できます。
React 19: ref コールバックの cleanup
ref には関数を渡すこともできます。このとき React 19 では cleanup 関数を返せる ようになりました。
<input
ref={(node) => {
console.log("attached:", node);
return () => {
console.log("detached");
};
}}
/>- 要素がマウントされたとき、コールバックが呼ばれる(
nodeは DOM ノード)。 - 要素がアンマウントされたとき、戻り値の関数が呼ばれる。
useEffect の cleanup と同じ発想で、外部ライブラリのインスタンスを生成・破棄するパターンが素直に書けるようになりました。
図 4.5.2 Ref のライフサイクル
Ref と state の使い分け
公式ドキュメントの整理が明快なので、そのまま引用します。
| 質問 | Ref | State |
|---|---|---|
| この値を表示に使うか? | 使わない | 使う |
| 変わったら再レンダーすべきか? | すべきでない | すべき |
| 直接代入できるか? | できる (ref.current = ...) | できない (setter 経由) |
迷ったら state で始めるのがおすすめです。Ref が必要になるのは、「これ、再レンダー起きたら無駄だな」と気づいた後、あるいは DOM を触る必要があることが明確になった後です。
アンチパターン: state と ref を二重管理しない
ある値を state にも入れ、ref にも入れる、というのは大抵バグの元になります。どちらが真かが曖昧になり、片方の更新を忘れるとズレます。もし「描画には state を使いたいが、コールバックからは即値で参照したい」という要求なら、useEffect で ref へ同期するのが一つの手です。
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);ただしこのパターンは複雑さを増やすので、本当に必要か を先に疑うようにしてください。