Reactの基礎 - useRef
概要
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要素の型を示す。
| 型名 | 対象要素 | 使用例 |
|---|---|---|
| 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.current が null になる場合があるため、オプショナルチェーン (?.) を使用して安全にアクセスする。
- フォーカス制御の例
- 以下の例では、ボタンを押下すると入力欄にフォーカスを当てている。
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の違い
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の保持
setInterval や setTimeout の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>
);
}
関連情報