useRef

useRef で DOM 要素や画面表示に使わない値を保持する方法を学ぶ。

useRef はレンダー間で値を保持する Hook です。ref.current を変えても再レンダーされないため、DOM 要素、タイマー ID、前回の値のように表示へ直接使わない情報に向いています。

DOM 要素を参照する

ref を JSX の ref 属性へ渡すと、React は DOM を作った後に current へ要素を設定します。

import { useRef } from 'react';

function SearchForm() {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <input ref={inputRef} type="search" />
      <button type="button" onClick={() => inputRef.current?.focus()}>
        検索欄にフォーカス
      </button>
    </>
  );
}

フォーカス、スクロール、テキスト選択のようにブラウザ API を操作するときは、イベントハンドラや Effect から ref を使います。

state と区別する

画面に表示する値は useState で管理します。たとえばクリック回数を ref.current に保存しても画面は更新されません。一方、setTimeout の返り値を保存して後で解除する用途なら再レンダーは不要なので ref が適しています。

const timerRef = useRef<number | null>(null);

function start() {
  timerRef.current = window.setTimeout(() => console.log('done'), 1000);
}

停止処理では同じ ref を使えます。値が null かを確認してから clearTimeout を呼び、解除後に null へ戻します。

function stop() {
  if (timerRef.current !== null) {
    window.clearTimeout(timerRef.current);
    timerRef.current = null;
  }
}

このように開始と終了が対になる外部リソースでは、ref は「現在どのリソースを所有しているか」を覚える場所になります。コンポーネントが unmount される場合は Effect の cleanup でも停止する必要があります。

この例では timer ID を画面に描画しないため、更新を React へ通知する理由がありません。反対に「タイマーが実行中か」をボタンの disabled 表示へ使うなら、その真偽値は state にします。1つの事実でも、画面に影響する部分としない部分を分けることが大切です。

落とし穴

レンダー中に ref.current を読み書きすると、React が同じ props と state から同じ UI を作るという前提を壊します。初期化時だけ予測可能に代入する特殊な場合を除き、イベントまたは Effect で更新します。また ref の変更は React DevTools で追跡しにくいため、UI state の代用にはしません。

DOM ref は要素が unmount されると null になります。そのため非 null を前提にしたメソッド呼び出しでは optional chaining や null 判定が必要です。子コンポーネントへ ref を渡す設計は命令的な結合を増やすので、通常の props で解決できないかを先に考えます。

ref で前回の値を覚える場合

前回の props を表示せず比較だけしたい場合にも ref を使えます。ただし、現在の props から導ける表示を ref へコピーしてはいけません。たとえば「検索語が変わったらアニメーションを開始する」という外部操作のために前回値を記録するのは妥当ですが、「前回の検索語」を画面に出すなら state にします。値が UI の原因なのか、外部操作の補助情報なのかで選びます。

まとめ

useRef は React の宣言的なデータフローから外部システムへ接続するための逃げ道です。DOM 参照と非表示の可変値に限定し、表示を変える値には useState を使います。

Sources