React: useRef と DOM の操作

最終更新:2026-10-26

useRef これはまるで金庫のようなものです。中に何でも収納でき、取り出してもその状態は変わりません。useStateとは異なり、refの値を変更しても、コンポーネントの再レンダリングはトリガーされません。


1. 学習内容



2. 検索バーの物語

useRefの2つの主な用途

100%
flowchart LR
    subgraph "Uses1: DOMQuote"
        A[useRef] --> B["ref={inputRef}"]
        B --> C["inputRef.current.focus()"]
    end
    
    subgraph "Uses2: Variable-Value Storage"
        D[useRef] --> E[".current = value]
        E --> F[Does not trigger a repaint]
    end
    
    G[forwardRef] --> H[Passed from the parent componentref]
    H --> I[Child Component ExposureDOM]
    
    style A fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32
    style G fill:#fff3e0,stroke:#e65100

(1) 課題:入力フィールドにフォーカスを当てるには、DOMを操作する必要がある

ボブは検索ページを作成し、ページが読み込まれた際に検索ボックスに自動的にフォーカスが当たるようにしたかった:

▶ 試してみよう

ボブはuseStateを使って、以下を実装したいと考えています:

▶ 試してみよう

(2) useRef を用いた解決策

▶ 試してみよう

出力:

TEXT
ボタン:Select All Text、Scroll to the bottom、Measure the height。入力:自動フォーカスの入力フィールド。useEffect が副作用を管理。useRef で DOM を参照

利点:useRef は可変オブジェクト { current: null } を返し、.current は実際の DOM 要素です。セレクタや getElementById は不要で、React と自然に連携します。



3. useRefの2つの主な用途

目的 ref.current が指す先 レンダリングの更新をトリガーする 代表的なシナリオ
DOM リファレンス DOM 要素 ❌ フォーカス、スクロール、寸法の測定
変数の格納 任意の JavaScript 値 ❌ タイマー ID、前回の値、レンダリングされていない状態

(1) ユースケース 1: DOM 参照

最も一般的な用途は、DOM要素の取得、フォーカスやサイズ、スクロール位置の操作などです。

操作 コード 説明
フォーカス inputRef.current.focus() オートフォーカス入力欄
テキストを選択 inputRef.current.select() 入力フィールド内のすべてを選択
スクロール先 divRef.current.scrollIntoView() 特定の要素までスクロール
寸法を読み取る divRef.current.offsetHeight 要素の高さを取得
動画を再生 videoRef.current.play() 動画再生の操作

▶ サンプル:5つの一般的なDOM操作

▶ 試してみよう

出力:

TEXT
自動フォーカスの入力フィールド、"Select All Text" ボタン、スクロール可能な 20 行リスト("Scroll to bottom" 付き)、"Measure the height" 付きのボックス → px 数値を表示

(2) ユースケース 2:変数の値を保存する(再描画をトリガーせずに)

これがuseRefの隠し技です。どんな値でも格納でき、その値が変わってもコンポーネントは再レンダリングされません。

▶ 試してみよう

4. useRef 対 useState

ディメンション useRef useState
戻り値 { current: initial } [value, setter]
変更方法 ref.current = newValue setState(newValue)
変更後のレンダリング ❌ トリガーされない ✅ 再レンダリングがトリガーされる
読み込まれるタイミング 最新の値を即座に読み込む 次のレンダリングが行われるまで読み込まない
ユースケース DOM操作、タイマーID、以前の値 UIに表示されるデータ
▶ 試してみよう

出力:

TEXT
表示:"DOM Example of Operation"。ボタン:Select All Text、Scroll to the bottom、Measure the height。入力:自動フォーカスの入力フィールド。useEffect が副作用を管理。useRef で DOM/変数を参照

▶ サンプル:useRef を使用して前の値を保存する

▶ 試してみよう

出力:

TEXT
現在:0、前回:(first time)。+1 をクリック → 現在:1、前回:0。クリックのたびに現在値と前の値を表示する。


5. forwardRef:親コンポーネントによる子コンポーネントのDOMの制御

デフォルトでは、関数コンポーネントは自身のDOM参照を公開しません。forwardRef を使用することで、子コンポーネントは親コンポーネントの ref を受け取ることができます:

▶ 試してみよう

6. useImperativeHandle: 特定のメソッドを公開する

DOM要素全体を公開したくない場合は、useImperativeHandle を使用して特定のメソッドのみを公開してください:

▶ 試してみよう

7. 完全な例:リッチテキストエディタ

▶ 実行例を展開(107 行)
▶ 試してみよう

出力:

TEXT
状態:count(setter: setCount)。ボタン:setCount(c => c + 1)}>+1。useEffect が副作用を管理。useRef で DOM/変数を参照

期待される出力:太字、斜体、下線、リスト、H2見出し、その他の書式設定オプションをサポートするリッチテキストエディタ。コンテンツは、ref を通じて外部で保存、読み込み、またはクリアすることができます。


▶ サンプル:useRef を使用して、外部からクリックされた際にポップアップウィンドウを閉じる

▶ 試してみよう

出力:

TEXT
"Menu ▾" ボタン → ドロップダウンメニュー(Profile、Settings、Logout)。外側をクリック → useClickOutside フックで自動的に閉じる。

出力:

TEXT
スロットルボタン:連打は無視される(最大 1 秒に 1 回)。クリックカウンター + "Throttled!" のフィードバック。useRef で lastClick のタイムスタンプを保存

▶ サンプル:useRef を使用してタイマーを保存し、スロットリング機能付きボタンを実装する

▶ 試してみよう

出力:

TEXT
クリック → "Clicked!" + カウンター。1 秒以内に再クリック → "Too fast! Wait 1 second."(赤)。useRef のタイムスタンプでスロットルを実装。

出力:

TEXT
表示:"Click Me"。状態:clicks(setter: setClicks)、feedback(setter: setFeedback)。ボタン:Click Me。useRef で DOM/変数を参照。タイマーベースの挙動

▶ サンプル:forwardRef を使用して、フォーカス可能な Input コンポーネントをラップする

JSX
const FancyInput = React.forwardRef(function FancyInput({ label, error, ...props }, ref) {
  const internalRef = useRef(null)

  useImperativeHandle(ref, () => ({
    focus: () => internalRef.current?.focus(),
    selectAll: () => {
      const el = internalRef.current
      if (el) { el.focus(); el.select() }
    },
    scrollIntoView: () => internalRef.current?.scrollIntoView({ behavior: 'smooth' }),
  }))

  return (
    <div style={{ marginBottom: 12 }}>
      <label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>{label}</label>
      <input
        ref={internalRef}
        style={{ width: '100%', padding: 8, borderRadius: 4, border: `1px solid ${error ? '#ff4d4f' : '#d9d9d9'}`, outline: 'none' }}
        {...props}
      />
      {error && <p style={{ color: '#ff4d4f', fontSize: 12, margin: '4px 0 0' }}>{error}</p>}
    </div>
  )
})

function FormWithFancyInput() {
  const emailRef = useRef(null)

  function handleSubmit(e) {
    e.preventDefault()
    emailRef.current?.focus()
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Sign Up</h3>
      <FancyInput ref={emailRef} label="Email" type="email" placeholder="you@example.com" />
      <FancyInput label="Password" type="password" placeholder="At least 6 characters" />
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
    </form>
  )
}
▶ 試してみよう

出力:

TEXT
登録フォーム:ラベル付きの Email と Password 入力フィールド、エラー表示のスタイル付き。送信時に email にフォーカス。focus()、selectAll()、scrollIntoView() メソッドを公開。

❓ よくある質問

Q すべてのコンポーネントで forwardRef を使用する必要がありますか?
A いいえ。以下のケースでのみ使用してください:① 親コンポーネントが子コンポーネントの DOM を直接操作する必要がある場合(例:入力フィールドにフォーカスを当てたり、メディアの再生を制御したりする場合);② 再利用可能なフォームコンポーネントをカプセル化する場合(親コンポーネントがフォーカスや選択を制御できるようにするため)。単にデータを渡すだけなら、props を使用すれば十分です。
Q useRef は useState の代わりになりますか?
A いいえ。useRef を変更してもコンポーネントの再レンダリングは発生しませんが、setState を変更した場合は再レンダリングが発生します。値を UI に表示する必要がある場合や、レンダリング結果に影響を与える必要がある場合は、useState を使用する必要があります。単に「ユーザーに表示する必要のないデータ」(インターバル ID、前回のレンダリング時の値、WebSocket インスタンスなど)を保存する場合は、useRef を使用してください。useRef は、「再レンダリングをトリガーしない変数コンテナ」と考えてください。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):マウントされた際に自動的にフォーカスを取得する AutoFocusInput コンポーネントを作成し、親コンポーネントから呼び出せる 2 つのメソッド、focus() と clear() を実装してください。
  2. 上級演習(難易度 ⭐⭐):ClickCounter コンポーネントを作成し、useRef を使用してクリック数を追跡し(UI には表示しない)、useEffect を使用して、レンダリングのたびにコンソールに「X 回クリックされました」とログを出力してください。
  3. 課題(難易度:⭐⭐⭐):InfiniteScroll コンポーネントを作成し、useRef を使用して、センチネル要素(ページ最下部の特定の要素)がビューポートに入ったことを検知し、それによってさらなるデータの読み込みをトリガーするようにします。IntersectionObserver API を使用してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%