第7回

useRefの使い方:DOM参照・前回値の保持・レンダリングを起こさない値

ReactのuseRefを解説します。DOM要素への直接参照、前回値の保持、レンダリングをトリガーしない値の管理など、useRefの3つの主要ユースケースを実例付きで学べます。

·13分で読める
たける
たける 検索ボックスのクリアボタンを押したあと、自動でinputにフォーカスを戻したいんですが、どうやってDOMに直接アクセスするんですか?
りこ
りこ useRefでDOM要素への参照を持つ。`ref={inputRef}` とJSXに渡すと、`inputRef.current` にそのDOM要素が入る。あとは `inputRef.current?.focus()` を呼ぶだけ。

useRefとは

useRef再レンダリングを引き起こさずに値を保持する Hookです。

const ref = useRef(initialValue);
// ref.current に値が入る

useState との違いは、ref.current を変更してもコンポーネントが再レンダリングされない点です。

useRefの主なユースケースは3つあります:

  1. DOM要素への参照(フォーカス・スクロール・アニメーション)
  2. 前回値の保持(変化を検知する)
  3. レンダリングをトリガーしない値の管理(インターバルIDなど)

DOM要素への参照

ref をJSX要素に渡すと、ref.current にそのDOM要素が入ります。

function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();  // DOMの focus() を直接呼ぶ
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="検索..." />
      <button onClick={focusInput}>フォーカス</button>
    </div>
  );
}

マウント時に自動フォーカス

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);  // マウント時に1回だけ

  return <input ref={inputRef} type="text" />;
}

スクロール制御

function ChatWindow() {
  const bottomRef = useRef<HTMLDivElement>(null);
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    // 新しいメッセージが来たら一番下にスクロール
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <div style={{ height: 400, overflowY: 'scroll' }}>
      {messages.map((msg, i) => <div key={i}>{msg}</div>)}
      <div ref={bottomRef} />  {/* スクロール先のアンカー */}
    </div>
  );
}

動画・音声のコントロール

function VideoPlayer({ src }: { src: string }) {
  const videoRef = useRef<HTMLVideoElement>(null);

  const play = () => videoRef.current?.play();
  const pause = () => videoRef.current?.pause();

  return (
    <div>
      <video ref={videoRef} src={src} />
      <button onClick={play}>再生</button>
      <button onClick={pause}>一時停止</button>
    </div>
  );
}

前回値の保持

useEffect を使って前回のprops/stateを保持できます。

function usePrevious<T>(value: T): T | undefined {
  const prevRef = useRef<T | undefined>(undefined);

  useEffect(() => {
    prevRef.current = value;  // レンダリング後に更新
  });

  return prevRef.current;  // 今回のレンダリングでは前回の値を返す
}

function PriceDisplay({ price }: { price: number }) {
  const prevPrice = usePrevious(price);

  const diff = prevPrice !== undefined ? price - prevPrice : 0;

  return (
    <div>
      <span>¥{price.toLocaleString()}</span>
      {diff !== 0 && (
        <span style={{ color: diff > 0 ? 'red' : 'blue' }}>
          ({diff > 0 ? '+' : ''}{diff})
        </span>
      )}
    </div>
  );
}

レンダリングをトリガーしない値の管理

タイマーIDや外部ライブラリのインスタンスなど、レンダリングに影響しない値を保持するのに適しています。

function Stopwatch() {
  const [elapsed, setElapsed] = useState(0);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (intervalRef.current !== null) return;
    intervalRef.current = setInterval(() => {
      setElapsed((prev) => prev + 100);
    }, 100);
  };

  const stop = () => {
    if (intervalRef.current === null) return;
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  const reset = () => {
    stop();
    setElapsed(0);
  };

  // クリーンアップ
  useEffect(() => () => stop(), []);

  return (
    <div>
      <p>{(elapsed / 1000).toFixed(1)}秒</p>
      <button onClick={start}>スタート</button>
      <button onClick={stop}>ストップ</button>
      <button onClick={reset}>リセット</button>
    </div>
  );
}

インターバルIDを useState で管理すると、IDが変わるたびに再レンダリングが起きてしまいます。useRef を使うことで無駄な再レンダリングを防げます。

useStateとuseRefの使い分け

目的 推奨
UIに表示する値 useState
変更してもUIが変わらない値 useRef
DOM要素への参照 useRef
前回値の記憶 useRef
// ❌ レンダリング不要な値にuseStateを使う(無駄な再レンダリング)
const [timerId, setTimerId] = useState<number | null>(null);

// ✅ useRefを使う
const timerIdRef = useRef<number | null>(null);

forwardRefでrefを子コンポーネントに渡す

カスタムコンポーネントに ref を渡したいときは forwardRef を使います。

import { forwardRef } from 'react';

type InputProps = {
  label: string;
  placeholder?: string;
};

// forwardRef でrefを受け取れるようにする
const LabeledInput = forwardRef<HTMLInputElement, InputProps>(
  ({ label, placeholder }, ref) => (
    <div>
      <label>{label}</label>
      <input ref={ref} placeholder={placeholder} />
    </div>
  )
);

// 使い方
function Form() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <LabeledInput ref={inputRef} label="名前" placeholder="入力してください" />
      <button onClick={() => inputRef.current?.focus()}>フォーカス</button>
    </div>
  );
}
たける
たける `useRef` と `useState` の根本的な違いがいまいちわかってなくて。どちらも値を保持しますよね。
りこ
りこ stateが変わると再レンダリングが起きる。refが変わっても起きない。「画面に反映する値はstate、内部処理だけで使う値はref」。タイマーのID・前回値・DOM参照はrefに入れる。

まとめ

  • useRef は再レンダリングを起こさずに値を保持する
  • DOM要素への参照(フォーカス・スクロールなど)に使う
  • 前回値の保持や、タイマーIDなどレンダリング不要な値の管理に適している
  • UIに反映する値は useState、内部で使うだけの値は useRef

次の第8回では、重い計算結果をメモ化して再計算を防ぐ useMemo を学びます。