【React】useRefの使い方:DOM参照と値の保持

JavaScript

こんにちは、かつコーチです。

前回は「useEffectの依存配列でハマらないためのコツ」を解説しました。

今回は、useEffectとセットでよく登場する useRef を扱います。

「DOM要素を直接触りたいとき」「再レンダリングを起こさずに値を保持したいとき」に欠かせないフックなので、この記事でしっかり使い方を押さえておきましょう。

useRefとは?

再レンダリングを引き起こさない「入れ物」

useRef は、コンポーネントの再レンダリングをまたいで値を保持できる入れ物(オブジェクト)を作るフックです。

useState と似ていますが、決定的な違いがあります。

useState は値を更新すると再レンダリングが走りますが、useRef は値を更新しても再レンダリングが起きません。

import { useRef, useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  const renderCountRef = useRef(0);

  renderCountRef.current = renderCountRef.current + 1;

  return (
    <div>
      <p>カウント: {count}</p>
      <p>レンダリング回数: {renderCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

renderCountRef.current はレンダリングのたびに更新されますが、この値自体を更新しても再レンダリングは発生しません。

なぜ再レンダリングされないと嬉しいのか

useState だけで値を管理していると、「表示には関係ないけれど保持しておきたい値」まで再レンダリングの対象になってしまいます。

タイマーIDや直前の入力値など、画面に表示しない値を持たせたいときは useRef の出番です。

useRefの2大用途

用途1:DOM要素への参照を取得する

useRef の代表的な使い道が、DOM要素への直接アクセスです。

import { useRef } from "react";

function TextInputWithFocus() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    inputRef.current?.focus();
  };

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

ref 属性にセットした inputRef は、レンダリング後に inputRef.current へ実際のDOM要素が入ります。

ボタンを押すと inputRef.current.focus() が呼ばれ、入力欄にカーソルが移動します。

TypeScriptで書く場合、useRef<HTMLInputElement>(null) のようにジェネリクス(型を差し込む仕組み)で要素の型を指定しておくと、.current に対する補完や型チェックが効くようになります。

用途2:レンダリングをまたいで値を保持する

もう1つの用途が、setTimeout のIDなど「表示には出さないが覚えておきたい値」の保持です。

import { useRef, useState } from "react";

function DebouncedSearch() {
  const [keyword, setKeyword] = useState("");
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;

    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }

    timerRef.current = setTimeout(() => {
      setKeyword(value);
      console.log("検索実行:", value);
    }, 500);
  };

  return <input onChange={handleChange} placeholder="検索キーワード" />;
}

timerRef.current にタイマーIDを保持しておくことで、次の入力があったときに前回のタイマーを確実にキャンセルできます。

つまずきやすいポイント:レンダリング中にrefを更新してしまう

かつコーチが実際にハマった例

私が最初に useRef を使ったとき、レンダリング中の処理の中で .current を書き換えてしまい、画面がちらつくような不安定な挙動になったことがあります。

原因は、本来イベントハンドラや useEffect の中で行うべき更新を、コンポーネント本体の中でそのまま実行してしまっていたことでした。

❌ Before:レンダリング処理の中でrefを更新してしまう

import { useRef } from "react";

function LogList({ items }: { items: string[] }) {
  const countRef = useRef(0);

  // レンダリングのたびにここが実行され、副作用が予測できなくなる
  countRef.current = countRef.current + items.length;

  return <p>累計アイテム数: {countRef.current}</p>;
}

このコードは、コンポーネントが再レンダリングされるたびに countRef.current を書き換えています。

Reactのレンダリングは「何度呼ばれてもよい純粋な処理」であるべきという前提が崩れ、Strict Modeの二重レンダリングなどで値がずれるバグにつながります。

✅ After:更新はuseEffectやイベントハンドラの中で行う

import { useEffect, useRef } from "react";

function LogList({ items }: { items: string[] }) {
  const countRef = useRef(0);

  useEffect(() => {
    countRef.current = countRef.current + items.length;
  }, [items]);

  return <p>累計アイテム数: {countRef.current}</p>;
}

useEffect の中で更新するようにすることで、「レンダリングが終わった後に1回だけ実行される」という安全なタイミングに処理を移せます。

レンダリング中はrefを読むだけ、更新はuseEffectかイベントハンドラの中で行う、というルールを徹底すると事故を防げます。

応用:前回の値を覚えておく「usePrevious」パターン

値の変化を検知したいときの定番テクニック

useRef は、「1つ前のレンダリング時点の値」を覚えておくカスタムフックを作るときにもよく使われます。

import { useEffect, useRef } from "react";

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T | undefined>(undefined);

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

function PriceDisplay({ price }: { price: number }) {
  const previousPrice = usePrevious(price);
  const isUp = previousPrice !== undefined && price > previousPrice;

  return (
    <p style={{ color: isUp ? "red" : "blue" }}>
      現在価格: {price}円
    </p>
  );
}

useEffect の中で ref.current を更新しているので、コンポーネント本体で読み取る previousPrice は常に「1つ前のレンダリング時点の値」になります。

このパターンは、価格の上下判定やアニメーションの開始条件など、「変化そのもの」を検知したい場面で応用が効きます。

まとめ

この記事のポイント

  • useRef は再レンダリングを引き起こさずに値を保持できるフック
  • 代表的な用途は「DOM要素への参照」と「値の保持」の2つ
  • レンダリング中に.currentを更新するのはNG。更新はuseEffectかイベントハンドラの中で行う
  • 「前回の値を覚えておく」usePreviousパターンなど、応用範囲が広い

次に読むべき記事

次回は「useContextでProps drillingを解消する」です。

コンポーネントの階層が深くなったときに便利なフックなので、ぜひ続けて読んでみてください。

→ 次の記事:useContextでProps drillingを解消する

タイトルとURLをコピーしました