---
title: "useRefの使い方｜DOM参照・値の保持とuseStateとの違い【React 19.3対応】"
url: "https://www.issoh.co.jp/tech/details/3279/"
published: 2024-08-05
updated: 2026-09-28
categories: ["React"]
publisher: "株式会社一創"
---

# 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の新フック](/tech/details/4111/)で扱っています。

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

`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月版】](/tech/details/3671/)にまとめています。

## レンダー中に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との違いを解説](/tech/details/9415/)で説明しています。

## 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で渡す新方式と移行手順](/tech/details/4382/)で詳しく扱っています。

### 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の書き方を実例で解説](/tech/details/2898/)のように、内部でrefを使いつつ管理を肩代わりするライブラリに任せる

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

## 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)` のように、初期値を明示して渡します。

## 関連記事

- [React Hooksの使い方｜useState・useEffectの基本とReact 19.2の新フック](/tech/details/4111/)
- [Reactフック（Hooks）とは？全18種の一覧とReact Compiler時代の使い分け【2026年8月版】](/tech/details/3671/)
- [React 19でforwardRefが不要に｜refをpropsで渡す新方式と移行手順](/tech/details/4382/)
- [React Compilerとは？自動メモ化の仕組みと導入方法・useMemoとの違いを解説](/tech/details/9415/)
- [Reactカスタムフックのベストプラクティス｜命名・返り値・メモ化の判断基準](/tech/details/4147/)

---

出典: [useRefの使い方｜DOM参照・値の保持とuseStateとの違い【React 19.3対応】](<https://www.issoh.co.jp/tech/details/3279/>)（株式会社一創）
