Reactの基礎 - useRef

提供: MochiuWiki : SUSE, EC, PCB

概要

useRef は、レンダリングに必要のない値への参照を保持するためのHookである。
基本構文は const ref = useRef(initialValue) であり、{ current: T } 構造のRefObjectを返す。

useRef の最大の特徴は、ref.current の値を変更しても再レンダリングが発生しない点である。
これは値の変更があるたびにコンポーネントを再描画する useState とは根本的に異なる動作である。

useRef の主な用途は以下の2つに大別される。

  • DOM要素への参照
    ref 属性にRefObjectを渡すことで、DOMノードへの直接アクセスが可能になる。
    フォーカス制御、スクロール操作、メディア再生等に利用する。
  • ミュータブルな値の保持
    レンダリングをまたいで値を保持し続けるが、画面描画には影響しない。
    タイマIDや前回の値、WebSocketインスタンスの保持に利用する。


TypeScriptでは、useRef<HTMLInputElement>(null) のように型引数を指定することで、型安全なDOM操作が可能になる。


DOM要素への参照

ReactではDOM要素を直接操作するために useRef を使用する。
ref 属性にRefObjectを渡すことにより、コンポーネントのマウント後に ref.current からDOMノードへアクセスできる。

TypeScriptでのDOM型指定

TypeScriptで useRef を使用する場合、型引数にDOM要素の型を指定する必要がある。
初期値には null を指定して、型引数でアクセスするDOM要素の型を明示する。

下表に、よく使用されるDOM要素の型を示す。

よく使用されるDOM要素の型
型名 対象要素 使用例
HTMLInputElement <input> テキスト入力、フォーカス制御
HTMLTextAreaElement <textarea> 複数行テキスト入力
HTMLButtonElement <button> ボタン操作
HTMLDivElement <div> スクロール制御、レイアウト操作
HTMLVideoElement <video> 動画再生・一時停止
HTMLCanvasElement <canvas> Canvas描画


TypeScriptでの基本的な型指定を以下に示す。

 import { useRef } from 'react';
 
 function MyComponent() {
    // HTMLInputElement型を指定し、初期値はnull
    const inputRef = useRef<HTMLInputElement>(null);
 
    // HTMLDivElement型を指定
    const divRef = useRef<HTMLDivElement>(null);
 
    // HTMLVideoElement型を指定
    const videoRef = useRef<HTMLVideoElement>(null);
 
    return (
       <div>
          <input ref={inputRef} />
          <div ref={divRef}>コンテンツ</div>
          <video ref={videoRef} src="video.mp4" />
       </div>
    );
 }


DOM操作の例

DOM要素への参照を活用した操作例を以下に示す。
ref.currentnull になる場合があるため、オプショナルチェーン (?.) を使用して安全にアクセスする。

  • フォーカス制御の例
    以下の例では、ボタンを押下すると入力欄にフォーカスを当てている。
     import { useRef } from 'react';
     
     export default function FocusExample() {
        const inputRef = useRef<HTMLInputElement>(null);
     
        const handleFocus = () => {
           // オプショナルチェーンで安全にアクセス
           inputRef.current?.focus();
        };
     
        return (
           <div>
              <input ref={inputRef} placeholder="テキストを入力" />
              <button onClick={handleFocus}>フォーカス</button>
           </div>
        );
     }
    

  • スクロール操作の例
    以下の例では、ボタンを押下すると特定の要素までスクロールしている。
     import { useRef } from 'react';
     
     export default function ScrollExample() {
        const targetRef = useRef<HTMLDivElement>(null);
     
        const handleScroll = () => {
           targetRef.current?.scrollIntoView({ behavior: 'smooth' });
        };
     
        return (
           <div>
              <button onClick={handleScroll}>ターゲットへスクロール</button>
              <div style={{ height: '200px' }} />
              <div ref={targetRef}>ここにスクロールする</div>
           </div>
        );
     }
    

  • メディア再生の例
    以下の例では、動画の再生・一時停止を制御している。
     import { useRef } from 'react';
     
     export default function VideoPlayer() {
        const videoRef = useRef<HTMLVideoElement>(null);
     
        const handlePlay = () => videoRef.current?.play();
        const handlePause = () => videoRef.current?.pause();
     
        return (
           <div>
              <video ref={videoRef} src="video.mp4" />
              <button onClick={handlePlay}>再生</button>
              <button onClick={handlePause}>一時停止</button>
           </div>
        );
     }
    



useRefとuseStateの違い

useRefuseState はどちらも値を保持するが、再レンダリングへの影響とミュータブル性に大きな違いがある。

下表に、両者の比較を示す。

useRefとuseStateの比較
項目 useRef useState
再レンダリング 値を変更しても発生しない。 値を変更すると発生する。
ミュータブル性 ref.current を直接変更可能 setState 関数を経由して変更する。
読み取り方法 ref.current で読み取る。 変数名で直接読み取る。
主な用途 DOM操作、タイマID、ミュータブルな値 UIに反映させる状態管理
TypeScript型 RefObject<T> または MutableRefObject<T> [T, Dispatch<SetStateAction<T>>]


両者の動作の違いを確認する。

 import { useRef, useState } from 'react';
 
 export default function Counter() {
    // useStateの場合 : 値を変更すると再レンダリングが発生する
    const [stateCount, setStateCount] = useState(0);
 
    // useRefの場合 : 値を変更しても再レンダリングは発生しない
    const refCount = useRef(0);
 
    const handleStateIncrement = () => {
       setStateCount(stateCount + 1);  // 再レンダリングが発生する
    };
 
    const handleRefIncrement = () => {
       refCount.current += 1;  // 再レンダリングは発生しない
       console.log('refCount:', refCount.current);  // コンソールには出力されるが画面は変わらない
    };
 
    return (
       <div>
          <p>useState: {stateCount} (画面に反映される)</p>
          <p>useRef: 変化しているが画面には反映されない</p>
          <button onClick={handleStateIncrement}>useState + 1</button>
          <button onClick={handleRefIncrement}>useRef + 1</button>
       </div>
    );
 }



ミュータブルな値の保持

useRef はDOMへの参照だけでなく、レンダリングをまたいで値を保持し続けるミュータブルなコンテナとしても使用できる。

前回の値の保持

前回のレンダリング時の値を保持したい場合、useRef を使用したカスタムHookパターンが有効である。

 import { useEffect, useRef } from 'react';
 
 // usePreviousカスタムHook : 前回の値を返す
 function usePrevious<T>(value: T): T | undefined {
    const ref = useRef<T | undefined>(undefined);
 
    useEffect(() => {
       // エフェクト内で更新することで、レンダリング後に前回の値が保存される
       ref.current = value;
    });
 
    return ref.current;
 }
 
 export default function PreviousValueExample() {
    const [count, setCount] = useState(0);
    const prevCount = usePrevious(count);
 
    return (
       <div>
          <p>現在の値: {count}</p>
          <p>前回の値: {prevCount}</p>
          <button onClick={() => setCount(count + 1)}>増加</button>
       </div>
    );
 }


タイマIDの保持

setIntervalsetTimeout のIDを保持してタイマを管理する場合、useRef が適切である。
タイマIDはUIの表示には不要であるため、useState ではなく useRef を使用する。

 import { useRef, useState } from 'react';
 
 export default function TimerExample() {
    const [count, setCount] = useState(0);
    const intervalIdRef = useRef<number | null>(null);
 
    const handleStart = () => {
       if (intervalIdRef.current !== null) return;  // 既に起動中なら何もしない
 
       intervalIdRef.current = window.setInterval(() => {
          setCount(prev => prev + 1);
       }, 1000);
    };
 
    const handleStop = () => {
       if (intervalIdRef.current === null) return;
 
       clearInterval(intervalIdRef.current);
       intervalIdRef.current = null;
    };
 
    return (
       <div>
          <p>カウント: {count}</p>
          <button onClick={handleStart}>開始</button>
          <button onClick={handleStop}>停止</button>
       </div>
    );
 }


WebSocketインスタンスの保持

WebSocketのようなミュータブルなオブジェクトを保持する場合も、useRef が適切である。
接続オブジェクト自体は画面に表示しないため、再レンダリングを発生させる必要がない。

 import { useEffect, useRef, useState } from 'react';
 
 export default function WebSocketExample() {
    const wsRef = useRef<WebSocket | null>(null);
    const [messages, setMessages] = useState<string[]>([]);
 
    useEffect(() => {
       // WebSocket接続をrefで保持する
       wsRef.current = new WebSocket('wss://example.com/ws');
 
       wsRef.current.onmessage = (event) => {
          setMessages(prev => [...prev, event.data]);
       };
 
       return () => {
          wsRef.current?.close();
       };
    }, []);
 
    const handleSend = (message: string) => {
       wsRef.current?.send(message);
    };
 
    return (
       <div>
          {messages.map((msg, i) => <p key={i}>{msg}</p>)}
          <button onClick={() => handleSend('Hello')}>送信</button>
       </div>
    );
 }



React 19でのrefの変更

React 19では、ref の扱いに重要な変更が加えられた。
forwardRef() を使用せずに ref を通常のpropとして渡せるようになり、ソースコードが大幅に簡潔になった。

ref as prop

React 18以前では、子コンポーネントに ref を渡すためには forwardRef() でコンポーネントをラップする必要があった。
React 19以降は、ref を通常のpropとして受け取れるため、forwardRef() が不要になった。

  • React 18以前の実装
    forwardRef() を使用してrefを転送する方法である。
     import { forwardRef, useRef } from 'react';
     
     interface MyInputProps {
        label: string;
     }
     
     // React 18以前 : forwardRefでラップが必要
     const MyInput = forwardRef<HTMLInputElement, MyInputProps>(
        function MyInput({ label }, ref) {
           return (
              <div>
                 <label>{label}</label>
                 <input ref={ref} />
              </div>
           );
        }
     );
     
     export default function App() {
        const inputRef = useRef<HTMLInputElement>(null);
     
        return <MyInput label="名前" ref={inputRef} />;
     }
    

  • React 19以降の実装
    ref を通常のpropとして受け取る方法である。
     import { useRef } from 'react';
     
     interface MyInputProps {
        label: string;
        ref?: React.Ref<HTMLInputElement>;
     }
     
     // React 19以降 : forwardRefは不要、refを通常のpropとして受け取る
     function MyInput({ label, ref }: MyInputProps) {
        return (
           <div>
              <label>{label}</label>
              <input ref={ref} />
           </div>
        );
     }
     
     export default function App() {
        const inputRef = useRef<HTMLInputElement>(null);
     
        return <MyInput label="名前" ref={inputRef} />;
     }
    



useImperativeHandle

useImperativeHandle は、親コンポーネントに公開するメソッドやプロパティを制御するHookである。
子コンポーネントの実装の詳細を隠蔽しつつ、親が必要とするAPIのみを公開できる。

 import { useRef, useImperativeHandle } from 'react';
 
 interface InputHandle {
    focus: () => void;
    clear: () => void;
    getValue: () => string;
 }
 
 interface MyInputProps {
    ref?: React.Ref<InputHandle>;
 }
 
 function MyInput({ ref }: MyInputProps) {
    const inputRef = useRef<HTMLInputElement>(null);
 
    // 親に公開するAPIを定義する
    useImperativeHandle(ref, () => ({
       focus() {
          inputRef.current?.focus();
       },
       clear() {
          if (inputRef.current) {
             inputRef.current.value = '';
          }
       },
       getValue() {
          return inputRef.current?.value ?? '';
       },
    }), []);  // 第3引数は依存配列
 
    return <input ref={inputRef} />;
 }
 
 export default function App() {
    const inputHandle = useRef<InputHandle>(null);
 
    return (
       <div>
          <MyInput ref={inputHandle} />
          <button onClick={() => inputHandle.current?.focus()}>フォーカス</button>
          <button onClick={() => inputHandle.current?.clear()}>クリア</button>
          <button onClick={() => console.log(inputHandle.current?.getValue())}>値を取得</button>
       </div>
    );
 }



コールバックref

ref 属性には、RefObjectだけでなくコールバック関数を渡すこともできる。
コールバックrefは、要素がマウントされたときにDOM要素が引数として渡され、アンマウントされた時に null が渡される。

 import { useCallback, useRef } from 'react';
 
 export default function CallbackRefExample() {
    // useCallbackでラップして不要な再呼び出しを防止する
    const inputCallbackRef = useCallback((node: HTMLInputElement | null) => {
       if (node !== null) {
          // マウント時 : DOM要素が渡される
          node.focus();
          console.log('マウント:', node);
       }
       else {
          // アンマウント時 : nullが渡される
          console.log('アンマウント');
       }
    }, []);  // 依存配列が空のため、関数は一度だけ生成される
 
    return <input ref={inputCallbackRef} placeholder="マウント時に自動フォーカス" />;
 }


コールバックrefは、要素のマウント・アンマウントのタイミングで処理を行う場合に特に有効である。
useCallback でラップしない場合、親コンポーネントが再レンダリングされるたびにコールバック関数が再生成され、不要な再呼び出しが発生するため注意が必要である。


よくある間違い

useRef の使用にあたって、よくある間違いと正しい対処法を以下に示す。

レンダリング中のref読み書き

render関数本体でのレンダリング中に ref.current を読み書きしてはいけない。
初期化処理等を除いて、ref.current へのアクセスはイベントハンドラや useEffect 内で行う。

 import { useEffect, useRef } from 'react';
 
 export default function WrongUsageExample() {
    const ref = useRef(0);
 
    // 正しい : useEffect内でrefを更新する
    useEffect(() => {
       ref.current += 1;
       console.log('レンダリング回数:', ref.current);
    });
 
    // 誤り : レンダリング中 (render関数本体) でrefを書き換えている
    // ref.current += 1;  // Reactの描画処理と競合するため禁止
 
    return <div>コンポーネント</div>;
 }


レンダリング用データへのref使用

画面に表示する値を useRef で管理してはいけない。
ref.current の変更は再レンダリングを発生させないため、画面に反映されない。

UIに表示する値は useState で管理する。

 import { useRef, useState } from 'react';
 
 export default function DataManagementExample() {
    // 正しい : 画面に表示する値はuseStateで管理する
    const [count, setCount] = useState(0);
 
    // 正しい : 画面に表示しない値はuseRefで管理する
    const renderCountRef = useRef(0);
    renderCountRef.current += 1;
 
    // 誤り : 画面に表示する値をrefで管理している
    // const countRef = useRef(0);
    // countRef.current += 1;  // 画面が更新されない
 
    return (
       <div>
          <p>カウント (画面に反映される): {count}</p>
          <p>レンダリング回数 (ログ用): {renderCountRef.current}</p>
          <button onClick={() => setCount(count + 1)}>増加</button>
       </div>
    );
 }



関連情報