1. ホーム
  2. React

【React】useRef の使い方|DOM 操作と再レンダリングしない値の保持を解説

Share

React で「入力欄にフォーカスを当てたい」「再描画とは関係なく値を覚えておきたい」というとき、useState ではうまくいかないことがあります。そんな場面で使うのが useRef フックです。useRef は「DOM 要素を直接触る」ための入り口であり、同時に「再レンダリングを起こさずに値を保持する箱」でもあります。この記事では、useRef の基本、DOM 操作での使い方、useState との違い、そして値の保持としての使い方までを、初心者向けに具体的なコードで解説します。

useRef の基本と ref オブジェクト

useRef(初期値) を呼ぶと、{ current: 初期値 } という形のオブジェクト(ref オブジェクト)が返ります。値には ref.current でアクセスし、ref.current = ... で書き換えます。最大の特徴は、current を書き換えても再レンダリングが起きないことです。

useRef-basic.tsx
import { useRef } from 'react';

function Example() {
  // { current: 0 } という箱が作られる
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current += 1;      // 書き換えても再レンダリングは起きない
    console.log(countRef.current); // クリックのたびに 1, 2, 3... と増える
  };

  return <button onClick={handleClick}>カウント</button>;
}

ボタンをクリックすると countRef.current は増えていきますが、画面の表示は変わりません。useRef が保持する値は再レンダリングをまたいで保たれ、かつ変更しても再描画のきっかけにはならないのです。この性質が、DOM 操作と「表示に出さない値の保持」という2つの用途につながります。

DOM 要素を操作する(もっとも多い用途)

useRef のいちばん代表的な使い方は、DOM 要素への参照を持つことです。JSX の要素に ref 属性を渡すと、React がマウント後にその DOM 要素を ref.current に入れてくれます。これを使えば、入力欄へのフォーカスやスクロール位置の操作など、React の外側の DOM 操作ができます。

focus-input.tsx
import { useRef } from 'react';

function SearchBox() {
  // 初期値は null。要素の型を指定しておく
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    // マウント後は current に input 要素が入っている
    inputRef.current?.focus();
  };

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

<input ref={inputRef} /> と書くと、React がその <input> の DOM 要素を inputRef.current にセットします。ボタンを押すと inputRef.current.focus() が呼ばれ、入力欄にカーソルが移動します。初期表示の時点では要素がまだ存在しないため currentnull です。そのため ?.(オプショナルチェーン)を付けて安全に呼び出しています。TypeScript では useRef<HTMLInputElement>(null) のように要素の型を指定しておくと、focus() などのメソッドを補完・型チェックできます。

useState との違いと使い分け

useRefuseState はどちらも「値を保持する」点で似ていますが、決定的な違いは変更したときに再レンダリングが起きるかどうかです。

観点useStateuseRef
値の変更で再レンダリング起きる起きない
値の読み書きstate / setState()ref.current を直接読み書き
画面表示への反映反映されるそのままでは反映されない
主な用途表示に使う状態DOM 参照・表示しない値の保持

判断の基準はシンプルです。その値が変わったときに画面を更新したいなら useState、更新する必要がないなら useRef を選びます。たとえば「入力された文字を画面に表示する」なら useState、「直前のタイマー ID を覚えておく」「フォーカス対象の要素を参照する」なら useRef です。逆に、表示に使う値を useRef で持つと、値は変わっているのに画面が古いまま、という不具合になります。

再レンダリングされても消えない値を持つ

DOM 参照以外に、useRef は「再レンダリングをまたいで値を覚えておきたいが、表示には使わない」ケースでも役立ちます。よくあるのが、setIntervalsetTimeout が返すタイマー ID の保持です。ID は表示に使いませんが、後で clearInterval するために覚えておく必要があります。

timer.tsx
import { useRef, useState } from 'react';

function Timer() {
  const [count, setCount] = useState(0);
  // タイマー ID を保持する箱(表示には使わない)
  const timerRef = useRef<number | null>(null);

  const start = () => {
    if (timerRef.current !== null) return; // 二重起動を防ぐ
    timerRef.current = window.setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  };

  const stop = () => {
    if (timerRef.current !== null) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };

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

ここでは表示する「経過秒数」は useState で管理し、内部的に必要なだけの「タイマー ID」は useRef で保持しています。もし ID を通常の変数で持つと、再レンダリングのたびに初期化されて clearInterval できなくなります。useRef なら値が保たれるので、「開始したタイマーを後から止める」といった処理が正しく書けます。

レンダリング中に current を読み書きしないよう注意

useRef でつまずきやすいのが、ref オブジェクトを JSX の中(レンダリング中)で直接読み書きしてしまうケースです。DOM 参照の current は初期レンダリング時点では null で、値がセットされるのはマウント後です。そのためレンダリング中に current を前提にした処理を書くと、初回に null エラーが起きたり、値が古いままになったりします。

ref.current の読み書きは、イベントハンドラの中や useEffect の中——つまりレンダリングが終わったあとのタイミングで行うのが原則です。また、current を変えても画面は更新されないため、「値を変えたら表示も変えたい」という用途なら useState を使う、という切り分けを忘れないようにしましょう。

まとめ

useRef{ current: 値 } という箱を返すフックで、2つの使い道があります。1つは ref 属性と組み合わせて DOM 要素を直接操作する使い方(フォーカス、スクロールなど)、もう1つは再レンダリングをまたいで値を保持する使い方(タイマー ID など表示しない値)です。共通する性質は「current を書き換えても再レンダリングが起きない」こと。だからこそ、画面に反映したい値は useState、反映しなくてよい値や DOM 参照は useRef、という使い分けになります。current の読み書きはイベントハンドラや useEffect の中で行い、レンダリング中には触らないことを意識すれば、useRef を安全に活用できます。

参考ページ