React

useRefの使い方|DOM参照・値の保持とuseStateとの違い【React 19.3対応】

useRefの使い方|DOM参照・値の保持とuseStateとの違い【React 19.3対応】

useRefは、再レンダーをまたいで同じオブジェクトを返し、その current プロパティに好きな値を入れておけるReactのフックです。使い方は大きく2つで、1つは input や section などのDOM要素を参照してフォーカスやスクロールを操作すること、もう1つはタイマーIDのように「覚えておきたいが画面には出さない値」を持つことです。useStateとの違いは、値を書き換えても再レンダーが起きない点に尽きます。

この記事では、React 19.3.0(2026年9月9日公開)と @types/react 19.3.0、eslint-plugin-react-hooks 7.1.1 でTypeScript例の型チェックとJavaScript例のリント結果を示しながら、基本の使い方から、レンダー中に ref.current を触ると出るエラー、React 19で変わったrefの扱いまでを順に説明します。

まとめ:useRefの使い方と押さえる要点

  • const ref = useRef(初期値) で作り、値は ref.current で読み書きする。同じコンポーネントでは毎回同じオブジェクトが返る
  • DOM参照は useRef(null) を要素の ref 属性に渡す。current に要素が入るのはコミット中のDOM更新後なので、イベントハンドラかエフェクトの中で使う
  • 値の保持(タイマーID・最新値・一度だけ作るオブジェクト)に使える。書き換えても再レンダーされないため、画面に表示する値はuseStateで持つ
  • レンダー中に ref.current を読み書きしない。eslint-plugin-react-hooks 7系の推奨設定では、refsルールが「Cannot access refs during render」として報告する(初期化の if (ref.current === null) だけは許される)
  • React 19以降は関数コンポーネントが ref を通常のpropとして受け取れ、refコールバックはクリーンアップ関数を返せる。TypeScriptでは useRef() の引数が必須になった

useRefとは:再レンダーをまたいで値を持つ入れ物

useRefは useRef(initialValue) の形で呼び、current プロパティを1つだけ持つオブジェクト(refオブジェクト)を返します。react.dev のリファレンスは戻り値について「On the next renders, useRef will return the same object.」と書いており、2回目以降のレンダーでは初期値は無視され、最初に作ったオブジェクトがそのまま返ります。

コンポーネント関数の中で普通に宣言した変数(let count = 0)はレンダーのたびに作り直されて値が消えます。useRefの current は消えません。一方でuseStateと違い、current を書き換えてもReactは再レンダーしません。つまりuseRefは「記憶はするが画面には影響しない」値のための仕組みです。

書き方 レンダーをまたいで保持 変更で再レンダー
関数内の let 変数 されない 起きない
useRef の current される 起きない
useState の state される 起きる

useRefでDOM要素を参照する使い方

DOM要素の参照は、useRefの代表的な用途です。useRef(null) で作ったrefを要素の ref 属性に渡すと、ReactがDOMノードを作った後で ref.current にそのノードを入れます。

ボタンで入力欄にフォーカスする最小コード

クリックで input にカーソルを移す例です。focus() はDOM標準のメソッドなので、inputRef.current に入ったDOMノードに対してそのまま呼べます。

import { useRef } from "react";

export default function SearchBox() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} placeholder="キーワード" />
      <button onClick={handleClick}>入力欄へ移動</button>
    </>
  );
}

scrollIntoViewによる指定要素へのスクロール

ページ内の目的の位置へ移動させるときは、スクロール先の要素にrefを付け、scrollIntoView() を呼びます。behavior: "smooth" でアニメーションし、block: "start" で、要素のブロック方向の開始端をスクロール領域の開始端に寄せます。通常の横書きでは上端に相当しますが、スクロール可能な範囲によっては一致しません。

import { useRef } from "react";

export default function Faq() {
  const answerRef = useRef(null);

  function handleJump() {
    answerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
  }

  return (
    <>
      <button onClick={handleJump}>回答へ移動</button>
      <div style={{ height: 1500 }} />
      <section ref={answerRef}>
        <h2>回答</h2>
      </section>
    </>
  );
}

ref.currentがnullになるタイミング

DOM参照のrefは null で初期化するのが定石です。最初のレンダーの時点では要素がまだ画面に無く、current に入れるものが無いからです。ReactはコミットフェーズでDOMを更新した後に current へノードを入れ、react.dev の記述どおり「React will set the current property back to null when the node is removed from the screen.」、つまり要素が消えると再び null に戻します。

このため、コンポーネント本体の処理中に inputRef.current.focus() を書くと、初回は null に対するメソッド呼び出しになり「TypeError: Cannot read properties of null」で落ちます。DOMを触る処理はクリックなどのイベントハンドラか、描画後に走る useEffect の中に置いてください。条件付きで表示する要素を参照する場合は inputRef.current?.focus() のようにnullを許す書き方が安全です。

useRefで値を保持する使い方

DOMに関係しない用途でも、useRefは「次のレンダーまで覚えておきたいが、変わっても画面を描き直す必要がない値」の置き場になります。代表的な3パターンを挙げます。

setIntervalのID保持とタイマー停止

ストップウォッチのように、開始時に作ったタイマーを後で止めるには、setInterval が返すIDをどこかに取っておく必要があります。IDは画面に表示しないのでstateにする理由がなく、refが適任です。アンマウント時に止め忘れないよう、クリーンアップでも clearInterval を呼んでいます。

import { useEffect, useRef, useState } from "react";

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());
    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  const seconds = startTime != null && now != null ? (now - startTime) / 1000 : 0;

  return (
    <>
      <p>経過: {seconds.toFixed(2)} 秒</p>
      <button onClick={handleStart}>開始</button>
      <button onClick={handleStop}>停止</button>
    </>
  );
}

経過秒数は画面に出すので startTime と now はstateで持ち、表示に使わないタイマーIDだけをrefにしている点が使い分けの見本です。

非同期処理から参照する最新値の保持

setTimeout に渡した関数は、登録した時点のレンダーの値を閉じ込めます(クロージャ)。3秒の間に入力が変わっても、そのまま text を読むと古い値が送られます。エフェクトでrefへ最新値を書き写しておくと、遅れて動く処理から最新の値を読めます。

import { useEffect, useRef, useState } from "react";

export default function DelayedSend() {
  const [text, setText] = useState("");
  const textRef = useRef(text);

  useEffect(() => {
    textRef.current = text;
  }, [text]);

  function handleSend() {
    setTimeout(() => {
      alert(`送信: ${textRef.current}`);
    }, 3000);
  }

  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handleSend}>3秒後に送信</button>
    </>
  );
}

エフェクトの中から「最新のpropsやstateを読むだけ」の関数を呼びたい場合は、React 19.2で安定版になった useEffectEvent も選択肢になります。新フックの位置付けはReact Hooksの使い方|useState・useEffectの基本とReact 19.2の新フックで扱っています。

再レンダー時のオブジェクト再生成を避ける初期化パターン

useRef(new VideoPlayer()) と書くと、初期値が使われるのは最初の1回だけなのに、new VideoPlayer() 自体はレンダーのたびに実行されます。react.dev は次のように null で作ってから初回だけ代入する書き方を示しています。

import { useRef } from "react";

class VideoPlayer {
  play() {}
}

export default function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }

  return <button onClick={() => playerRef.current.play()}>再生</button>;
}

レンダー中に current を読み書きするのは原則禁止ですが、react.dev はこの形について「it’s fine in this case because the result is always the same」と例外扱いにしています。eslint-plugin-react-hooks 7.1.1 で検査しても、この if (playerRef.current === null) の形はエラーになりません。

useRefとuseStateの違いと使い分け

useStateは、値と更新関数の組を返し、更新関数で値を変えるとReactがコンポーネントを再レンダーして画面に反映するフックです。useRefとの違いを表にまとめます。

観点 useState useRef
戻り値 [値, 更新関数] { current: 値 }
値の変え方 更新関数を呼ぶ current に直接代入
変更後の再レンダー 起きる 起きない
変更の反映 次のレンダーから 代入した直後から
レンダー中の読み取り できる しない(初期化を除く)
向いている値 画面に表示する値 DOMノード・タイマーID等

次は、stateとrefの更新の違いを示す比較用の失敗例です。レンダー中にrefを読むためリント違反となり、実装には使わないでください。stateのボタンは押すたびに数字が増えます。refのボタンは内部で current が増えていても、refのボタンだけを押している間は表示が変わりません。その後stateのボタンを押すと再レンダーが起き、そこで初めてrefの表示にも蓄積した値が現れます。

import { useRef, useState } from "react";

export default function Counters() {
  const [stateCount, setStateCount] = useState(0);
  const refCount = useRef(0);

  return (
    <>
      <button onClick={() => setStateCount(stateCount + 1)}>
        state: {stateCount}
      </button>
      <button onClick={() => { refCount.current += 1; }}>
        ref: {refCount.current}
      </button>
    </>
  );
}

判断基準は1つで、その値が変わったときに画面を描き直す必要があるならuseState、無いならuseRefです。迷ったらuseStateを選んでください。refに入れた値を表示に使うと、上の例のように「値は変わったのに画面が古い」不具合になり、原因の特定に時間がかかります。フック全体の選び方はReactフック(Hooks)とは?全18種の一覧とReact Compiler時代の使い分け【2026年8月版】にまとめています。

レンダー中にref.currentを読み書きしてはいけない理由

上のカウンターの例で出るリントエラーを確認します。React Compiler 由来のルールを含む eslint-plugin-react-hooks 7.1.1 の推奨設定(configs.flat.recommended)でこのコードを検査すると、react-hooks/refs ルールが次のエラーを出しました。

src/Counters.jsx
  13:15  error  Error: Cannot access refs during render

React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef).

> 13 |         ref: {refCount.current}
     |               ^^^^^^^^^^^^^^^^ Cannot access ref value during render
                                        react-hooks/refs

Reactはコンポーネントを「同じpropsとstateなら同じ結果を返す純粋な関数」として扱う前提で、レンダーを中断したりやり直したりします。refは再レンダーを起こさない値なので、レンダー中にそれを読んで表示を決めると、値が変わっても画面が追従しません。書き込みも同様で、Strict Modeの開発時の二重レンダーや並行レンダーの中断によって、書いた回数が実際の表示回数とずれます。react.dev もリファレンスの注意書きで「Do not write or read ref.current during rendering, except for initialization.」と明記しています。

このエラーが出たときの直し方は、値の性質で決まります。

  • 画面に表示する値なら、useStateに移す
  • 表示しない値なら、読み書きをイベントハンドラかエフェクトの中へ移す
  • 一度だけ作るオブジェクトなら、前章の if (ref.current === null) の初期化パターンにする

React Compiler はこのルールに違反したコンポーネントやフックを自動メモ化の対象から外し、残りのコードだけを最適化します。導入の前に全件を直しきる必要はなく、導入後に段階的にrefの扱いを修正できます。導入手順はReact Compilerとは?自動メモ化の仕組みと導入方法・useMemoとの違いを解説で説明しています。

React 19以降で変わったrefの扱い

2019年頃のHooks解説記事と現在とで書き方が変わった点をまとめます。古い記事のコードをそのまま写すと、不要なラッパーを書いたり、TypeScriptの型エラーで止まったりします。

子コンポーネントへのref propの受け渡し

React 18までは、自作の関数コンポーネントでrefを受け取るには forwardRef で包む必要がありました。React 19からは関数コンポーネントが ref を通常のpropとして受け取れます。TypeScriptでは ComponentProps<"input"> に ref が含まれるため、次のコードは @types/react 19.3.0 でそのまま型チェックを通ります。

import { useRef, type ComponentProps } from "react";

function MyInput({ ref, ...props }: ComponentProps<"input">) {
  return <input ref={ref} {...props} />;
}

export default function Form() {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <MyInput ref={inputRef} placeholder="氏名" />
      <button onClick={() => inputRef.current?.focus()}>氏名を入力</button>
    </>
  );
}

子コンポーネントの中のDOMノードをそのまま渡さず、focus などの限られたメソッドだけを公開したいときは useImperativeHandle を併用します。forwardRef からの移行手順とcodemodはReact 19でforwardRefが不要に|refをpropsで渡す新方式と移行手順で詳しく扱っています。

refコールバックのクリーンアップ関数

ref 属性にはrefオブジェクトの代わりに関数(refコールバック)も渡せます。React 19から、この関数はクリーンアップ関数を返せるようになりました。要素のサイズを ResizeObserver で監視し、要素が外れたら監視を止める例です。

import { useCallback, useState } from "react";

export default function SizeLabel() {
  const [width, setWidth] = useState(0);

  const measureRef = useCallback((node) => {
    const observer = new ResizeObserver(([entry]) => {
      setWidth(entry.contentRect.width);
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, []);

  return <div ref={measureRef}>幅: {Math.round(width)}px</div>;
}

ここで useCallback を使っているのには理由があります。react.dev は、レンダーのたびに別の関数を渡すと、前の関数のクリーンアップと次の関数の呼び出しが毎回行われると説明しています。jsdom 上で React 19.3.0 を動かして確かめると、インラインで ref={(node) => {...}} と書いた場合、stateを1回更新しただけでクリーンアップと再登録がもう1回走りました。監視やイベント登録のような処理をrefコールバックに書くなら、関数の参照を固定してください。なお、クリーンアップ関数を返さない場合は従来どおり null を引数にもう一度呼ばれますが、react.dev はこの挙動を「This behavior will be removed in a future version.」としています。

@types/react 19におけるuseRefの引数必須化

@types/react 19系では、useRef() を引数なしで呼ぶと「error TS2554: Expected 1 arguments, but got 0.」になります(18.3.31 では引数を省略できました)。また18系では useRef<HTMLInputElement>(null) の戻り値が current を読み取り専用にした RefObject でしたが、19系では RefObject<HTMLInputElement | null> になり、current へ代入できます。書き換え可能なrefを表していた MutableRefObject は @deprecated Use `RefObject` instead. となりました。

import { useRef } from "react";

export function Example() {
  const inputRef = useRef<HTMLInputElement>(null);
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const countRef = useRef(0);

  function reset() {
    countRef.current = 0;
    if (timerRef.current !== null) clearInterval(timerRef.current);
  }

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

タイマーIDの型はブラウザでは number、Node.jsの型定義が入った環境では NodeJS.Timeout と環境で変わるため、ReturnType<typeof setInterval> と書いておくとどちらでも通ります。

React 19.3のFragment ref

2026年9月9日公開のReact 19.3.0で、<Fragment> にもrefを付けられるようになりました(リリースノートの表記は「Fragment Refs: Add Refs to <Fragment />」)。refに入るのはDOMノードではなく FragmentInstance というオブジェクトで、囲んだ子要素群に対して focus・focusLast・blur・addEventListener・observeUsing(IntersectionObserver / ResizeObserver の登録)・scrollIntoView などを呼べます。

import { Fragment, useRef } from "react";

export default function AddressFields() {
  const groupRef = useRef(null);

  return (
    <>
      <Fragment ref={groupRef}>
        <label>郵便番号 <input name="zip" /></label>
        <label>住所 <input name="address" /></label>
      </Fragment>
      <button onClick={() => groupRef.current.focus()}>住所欄から入力</button>
    </>
  );
}

focus() は react.dev の説明どおり、子孫を深さ優先でたどって最初にフォーカスできる要素に当てます。入力欄のまとまりにフォーカスを移すためだけに div で包んでrefを付けていたコードは、レイアウトを崩す余計な要素を減らせます。19.3より前のReactでは使えないので、ライブラリで使う場合はpeerDependenciesの下限を確認してください。

useRefを使うべきでない場面

useRefは「Reactの仕組みの外に出る」ための道具です。次の場面では別の手段のほうが不具合を生みにくくなります。

  • 表示する値を持つ:再レンダーされず表示が古くなる。useStateを使う
  • propsやstateから計算できる値を持つ:レンダー中にそのまま計算すればよく、refに写す必要はない
  • useEffectの依存配列に ref.current を入れる:refの変更は再レンダーを起こさないので、依存配列に書いても変化を検知できない。要素の出入りを知りたいならrefコールバックを使う
  • Reactが管理するDOMを直接書き換える:textContent の書き換えや子要素の削除をrefで行うと、次の更新でReactの持つ状態と食い違う。フォーカス・スクロール・サイズ計測のような、Reactが扱わない操作に限る
  • フォームの全入力値をrefで集める:入力欄が数個なら問題ないが、検証やエラー表示まで含めるとrefの手管理は破綻しやすい。React Hook Formとは?使い方・バリデーション・v7の書き方を実例で解説のように、内部でrefを使いつつ管理を肩代わりするライブラリに任せる

refを使う処理が複数のコンポーネントで重複してきたら、useInterval のようなカスタムフックに切り出すと、refの読み書きを1か所に閉じ込められます。命名や返り値の設計はReactカスタムフックのベストプラクティス|命名・返り値・メモ化の判断基準を参照してください。

useRefに関するよくある質問

useRefとuseStateの違いは何ですか?

値を変えたときに再レンダーが起きるかどうかです。useStateは更新関数で値を変えると再レンダーして画面に反映し、useRefは current に代入しても再レンダーしません。画面に表示する値はuseState、表示しないDOMノードやタイマーIDはuseRefで持ちます。

useRefの初期値にnullを渡すのはなぜですか?

DOM参照のrefは、最初のレンダーの時点では対象の要素がまだ存在しないためです。ReactはDOMの更新後に current へ要素を入れ、要素が消えると null に戻します。TypeScriptでは useRef<HTMLInputElement>(null) と書くと RefObject<HTMLInputElement | null> 型になります。

ref.currentを変更しても画面が更新されないのはなぜですか?

refの変更はReactに通知されず、再レンダーが起きないためです。表示に使う値ならuseStateに移してください。レンダー中に ref.current を表示に使うコードは、eslint-plugin-react-hooks 7系の推奨設定などでreact-hooks/refsを有効にすると、「Cannot access refs during render」と報告されます。

useEffectの依存配列にref.currentを入れても動かないのはなぜですか?

依存配列はレンダーのたびに比較されますが、ref.current を書き換えてもレンダーが起きないため、比較の機会が来ません。要素が付いた・外れたタイミングで処理したい場合は、refコールバックとクリーンアップ関数を使います。

TypeScriptでuseRef()を引数なしで書くとエラーになるのはなぜですか?

@types/react 19系で useRef の引数が必須になったためです。値が無い状態から始めるなら useRef<number | undefined>(undefined) や useRef<HTMLDivElement>(null) のように、初期値を明示して渡します。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

ほか 2 件の記事からもリンクされています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  3. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動
  4. 2026.10.09 テックブログ スタディサプリの不正アクセスとメールアドレス3,687件|アカウント列挙を防ぐ実装
  5. 2026.10.08 コラム 雇用保険の適用拡大:2028年10月の週10時間以上への変更と、勤怠・労務システムで直す判定ロジック

RELATED POSTS 関連記事

目次