Skip to content

4.5 Refs — エスケープハッチ ​

React の基本は「state と props を変えれば、React が DOM を同期してくれる」ことでした。でも、たまに React に面倒を見てもらわずに、自分で直接操作したい 場面に出会います。そのための脱出口(escape hatch)が Ref です。公式ドキュメントも Ref を「エスケープハッチ」と呼んでいます。名前の通り、例外的な使い方のための道具だと思ってください。

Ref の 2 つの用途 ​

Ref の使い道は、次の 2 つに整理できます。

  1. DOM 要素への参照: 入力欄に focus を当てる、要素のサイズを測る、サードパーティライブラリに DOM を渡す、など。
  2. レンダーに影響しない可変値の保管場所: setInterval の ID、前回の値、外部ライブラリのインスタンスなど。state に入れると毎回再レンダーされてしまう値を、こっそり保持しておく場所です。

共通しているのは、Ref の変更では再レンダーが起きない ことです。再レンダーさせたい値は state、させたくない値は Ref、と覚えるとすっきりします。

図 4.5.1 Ref と state の住み分け ​

Ref(React の外で持つもの)・ 表示に使わない値・ 変わっても再レンダー不要・ DOM ノードへの参照・ 例: timer ID、外部ライブラリのインスタンスState(React が管理するもの) ・ 表示に使う値・ 変わったら再レンダーしたい・ 読み取り専用(setter 経由で更新)・ 例: count、入力値、選択中のタブ

useRef の基本形 ​

tsx
// react.dev/reference/react/useRef より
import { useRef } from "react";

const ref = useRef(initialValue);
// ref.current が実体

戻り値は { current: initialValue } 相当のオブジェクトで、毎レンダー同じ参照 が返ります(ここが useState と違うポイント)。ref.current は直接代入で変更できますが、React はそれを検知しません。

用途 1: DOM を参照する ​

tsx
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 のライフサイクル

(ボタンを押すと 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 を取るので、外部から子要素を消したり追加したりすると破綻してしまいます。

tsx
// NG: React が children を管理している要素を直接消す
divRef.current?.removeChild(...);

自分が作った要素(React が children を管理していない領域)なら触って構いません。

用途 2: レンダー外の可変値 ​

タイマー ID や、前回の props の値など、「表示には使わないけれど次のレンダーまで保持したい」値を持つ場所として使います。

tsx
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 を読み書きしない ​

tsx
// 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 で包む必要があります。

tsx
// 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 になっています。

tsx
// 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 関数を返せる ようになりました。

tsx
<input
  ref={(node) => {
    console.log("attached:", node);
    return () => {
      console.log("detached");
    };
  }}
/>
  • 要素がマウントされたとき、コールバックが呼ばれる(node は DOM ノード)。
  • 要素がアンマウントされたとき、戻り値の関数が呼ばれる。

useEffect の cleanup と同じ発想で、外部ライブラリのインスタンスを生成・破棄するパターンが素直に書けるようになりました。

図 4.5.2 Ref のライフサイクル ​

Ref と state の使い分け ​

公式ドキュメントの整理が明快なので、そのまま引用します。

質問RefState
この値を表示に使うか?使わない使う
変わったら再レンダーすべきか?すべきでないすべき
直接代入できるか?できる (ref.current = ...)できない (setter 経由)

迷ったら state で始めるのがおすすめです。Ref が必要になるのは、「これ、再レンダー起きたら無駄だな」と気づいた後、あるいは DOM を触る必要があることが明確になった後です。

アンチパターン: state と ref を二重管理しない ​

ある値を state にも入れ、ref にも入れる、というのは大抵バグの元になります。どちらが真かが曖昧になり、片方の更新を忘れるとズレます。もし「描画には state を使いたいが、コールバックからは即値で参照したい」という要求なら、useEffect で ref へ同期するのが一つの手です。

tsx
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
  countRef.current = count;
}, [count]);

ただしこのパターンは複雑さを増やすので、本当に必要か を先に疑うようにしてください。

参照ソース ​