サイトアイコン 【TechGrowUp】

useRef徹底ガイド──DOMアクセスからミューテーション管理まで

useRefとは?

ReactのuseRefフックは、レンダー間で変化しても再レンダーを引き起こさない「箱」を提供します。返されるオブジェクトは{ current: 値 }という形で、コンポーネントのライフサイクル全体を通じて同じ参照を保持します。主な用途は次の二つです。

  1. DOM要素への参照:フォーム入力フォーカスやスクロール位置制御
  2. ミューテーション値の保持:レンダー外で更新されるカウンタや外部ライブラリのインスタンス
import { useRef, useEffect } from 'react';

function TextInputFocus() {
  const inputRef = useRef(null);

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

  return <input ref={inputRef} placeholder="自動でフォーカス" />;
}

仕組みと特徴

この振る舞いにより、状態のように保持しつつUIに影響を与えないデータ を格納できます。

DOMリファレンスの実践例

フォームのフォーカス管理

複数ステップのフォームで次の入力要素へフォーカスを移すとき、useRef が活躍します。

function MultiStepForm() {
  const step1Ref = useRef(null);
  const step2Ref = useRef(null);
  const [step, setStep] = useState(1);

  useEffect(() => {
    if (step === 1) step1Ref.current.focus();
    if (step === 2) step2Ref.current.focus();
  }, [step]);

  return (
    <>
      {step === 1 && <input ref={step1Ref} />}
      {step === 2 && <input ref={step2Ref} />}
      <button onClick={() => setStep(s => s + 1)}>次へ</button>
    </>
  );
}

スクロール制御

チャットアプリで新しいメッセージ追加時に最下部までスクロールする例。

function ChatWindow({ messages }) {
  const bottomRef = useRef(null);

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <div className="chat-area">
      {messages.map(m => <p key={m.id}>{m.text}</p>)}
      <div ref={bottomRef} />
    </div>
  );
}

ミューテーション値の保持

レンダー回数を数える

状態にすると無限ループするような値は useRef で保持。

function RenderCount() {
  const countRef = useRef(0);
  useEffect(() => { countRef.current += 1 });
  return <p>レンダー回数: {countRef.current}</p>;
}

setIntervalのIDを保持

function Timer() {
  const intervalRef = useRef(null);
  const [count, setCount] = useState(0);

  const start = () => {
    if (intervalRef.current === null) {
      intervalRef.current = setInterval(() => setCount(c => c + 1), 1000);
    }
  };

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

  useEffect(() => stop, []); // アンマウント時に停止

  return <>
    <p>{count} 秒経過</p>
    <button onClick={start}>開始</button>
    <button onClick={stop}>停止</button>
  </>;
}

forwardRefと組み合わせる

親コンポーネントから子のDOMへ直接アクセスする場合は forwardRef が必要です。

const FancyInput = React.forwardRef((props, ref) => {
  return <input className="fancy" ref={ref} {...props} />;
});

function Parent() {
  const inputRef = useRef(null);
  return <>
    <FancyInput ref={inputRef} />
    <button onClick={() => inputRef.current.focus()}>フォーカス</button>
  </>;
}

useImperativeHandleで公開APIを制御

子コンポーネント側で ref 経由の公開メソッドをカスタマイズできます。

const Canvas = React.forwardRef((props, ref) => {
  const canvasRef = useRef();
  useImperativeHandle(ref, () => ({
    clear: () => canvasRef.current.getContext('2d').clearRect(0,0,300,150)
  }));
  return <canvas ref={canvasRef} width={300} height={150} />;
});

function DrawingApp() {
  const canvasRef = useRef();
  return <>
    <Canvas ref={canvasRef} />
    <button onClick={() => canvasRef.current.clear()}>クリア</button>
  </>;
}

コールバックrefとの比較

種類設定方法特徴
string refref="myRef"レガシー、廃止予定
callback refref={node => { ... }}動的に複数refや条件分岐を管理
useRef + ref属性const r=useRef(); … ref={r}一般的。オブジェクトに一度だけセット

パフォーマンスに与える影響

useRef はレンダーをトリガーしないため、頻繁に変化する値を保持してもコストは低い。ただし current の変更にUIを反映したい場合は、別途 useState と組み合わせる必要がある。

よくある落とし穴

  1. refがnullのままアクセス
    • 初回レンダー直後はまだDOMがセットされておらずnull。useEffect の中で読む。
  2. currentを書き換えても画面が更新されない
    • useRefは状態管理ではない。UIに反映が必要なら useState を使う。
  3. 依存配列にref.currentを入れてしまう
    • ref.current の値が変わっても依存配列は検知しない。監視には別状態が必要。

総括

useRefDOMアクセスミューテーション値の永続化 を実現する万能ポケットです。再レンダーコストを抑えつつ、状態とは異なる形で情報を保持できるため、フォーム・アニメーション・外部ライブラリ統合など多様な場面で活用できます。ポイントは「UI描画に関与しない値」を見極め、状態との役割分担を明確にすることです。

モバイルバージョンを終了