💨

(React) useRefについてまとめてみました。

に公開

useRef

これまで useRef は、レンダリング結果から取得した DOM ノードにアクセスするために使われることが多かったです。

例えば、以下のように input 要素にフォーカスを当てるケースが代表的でしょう。

import { useRef, useEffect } from "react";

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

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

しかし useRef はもっと奥深いところがありました。
レンダー間で値を保持するインスタンス変数としての性質を活かすことで、
ゲームループやタイマー管理、外部ライブラリのインスタンス保持、
さらに「前回の state の記憶」など、さまざまな場面で活躍してくれました。

今日は、その「インスタンス変数」としての性質を活かすuseRefについてまとめてみます。


レンダー間で値を保持するインスタンス変数

  • useRef(initialValue){ current: 初期値 } のオブジェクトを返します。
    初期値はマウント時に一度だけ適用され、再レンダーでは無視されます。
  • .current を更新しても再レンダーは発生しません。
    ※「強制的に再レンダーが必要な場合」は、useStateforceUpdate などで明示的に制御する必要があります。
  • 再レンダー不要な内部変数を安全に保持できるため、タイマー管理や一意ID発行などに最適です。

例 1. 定期的にアイテムを生成する

設定時間が経過すると新しいアイテムを生成するロジックです。

const spawnTimer = useRef(0); // 初期値はマウント時のみ
const nextId = useRef(0);

const update = useCallback((dt: number) => {
  // 経過時間を加算
  spawnTimer.current += dt;

  // spawnIntervalに到達したらアイテムを生成
  if (spawnTimer.current >= spawnInterval) {
    // タイマーをリセット(再レンダーなし)
    spawnTimer.current = 0;

    // 新アイテム生成(ID発行も内部refで管理)
    const newItem = {
      id: nextId.current++,  // IDが変わっても再レンダーしない
      x: Math.random() * 400
      y: 40,
      speed: Math.random() * (5 - 2) + 2,
      size: 20
    };
    setItems(prevItems => [...prevItems, newItem]); // この部分でのみレンダーが発生
  }
}, [setItems, spawnInterval]);

このコードは、次のようにイメージできます。

  1. スピード違反取り締まりタイマー(spawnTimer)
  • 毎フレーム「前フレームからの経過時間(dt)」を足し合わせ、一定時間ごとに処理を実行
  • 値を更新してもレンダーは起こらず、内部だけで時間を管理
  1. 番号発行機(nextId)
  • アイテムごとに一意のIDを付与
  • こちらも内部カウンターとして保管し、再レンダーを防止
  1. アイテム生成の仕組み
  • タイマーがしきい値を超えたらspawnTimerをリセット
  • nextIdの番号を使って新しいアイテムを作成
  1. アイテム配列の更新(setItems)で初めて画面がリレンダー

こうすることで、**「内部で値を保持しつつ必要なときだけレンダー」**が実現できます。

例 2. IntervalIDを固定

  • IntervalRefを使ってIDを取得
  • useStateを使って1秒ごとに数字を増加させる
import React, { useRef, useState } from "react";
import "./styles.css";

export default function App() {
  // インターバルIDを保持するRef
  const intervalRef = useRef(null);
  // カウンターを保持するState
  const [counter, setCounter] = useState(0);

  const handleClick = () => {
    // 既存のタイマーがあればクリア
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
    }
    // 新しいインターバルを開始してIDをRefに保存
    intervalRef.current = window.setInterval(() => {
      setCounter((c) => c + 1);
    }, 1000);
  };

  const closeClick = () => {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
      intervalRef.current = null; // 停止後はnullにリセット
    }
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <button onClick={handleClick}>カウンターをスタート</button>
      <button onClick={closeClick}>カウンターをストップ</button>
      <p>Interval ID (Ref): {intervalRef.current ?? "—"}</p>
      <p>カウント数 (State): {counter}</p>
    </div>
  );
}

ボタンとクリックして試してみましょう

まとめ

  • useRef は「インスタンス変数」として、再レンダーが不要な値を保持する場面で大活躍
  • 初期値はマウント時に一度だけ設定され、以降の再レンダーでは無視される。
  • .current 更新はレンダーを起こさないため、タイマーIDや内部カウンタなどの管理に最適。
  • 外部ライブラリのインスタンスやタイマーを使う場合は、useEffect のクリーンアップ(destroy()/clearInterval())を忘れずに。
  • 状態管理(state)内部変数(ref) を使い分けることで、余計な再レンダーを防ぎつつ、
    シンプルでパフォーマンスの高い実装が可能になります。

Discussion