React forwardRefの実務的な使い方|実装コード付き解説
Reactで開発していると、カスタムコンポーネントからDOM要素に直接アクセスしたいという場面が必ず出てきます。そんな時に活躍するのがforwardRefです。本記事では、教科書的な説明ではなく、実務で本当に使うコードを交えながら解説します。
1. forwardRefの簡易的な解説
Reactでは、デフォルトではrefをpropsとして渡すことができません。これはReactの設計思想に基づいているのですが、実務では「特定のDOM要素にアクセスしたい」という要求は頻繁に出現します。
forwardRefは、その名の通り親コンポーネントから受け取ったrefを子コンポーネント内のDOM要素に「転送」する機能です。
基本的なイメージは以下の通りです:
import { forwardRef } from 'react';
const CustomInput = forwardRef<HTMLInputElement, { placeholder: string }>(
(props, ref) => {
return <input ref={ref} placeholder={props.placeholder} />;
}
);
ここで重要なのは、関数コンポーネントの第2引数としてrefが渡されるということです。通常のpropsではrefは渡されませんが、forwardRefを使うことで可能になります。
2. 業務でのユースケース
forwardRefは「何でも使えばいい」というものではなく、実務では以下のような場面で活躍します。
2.1 フォーム入力値を直接取得したい場合
特に大規模なフォーム処理では、React Hookformなどのライブラリと組み合わせて、カスタム入力コンポーネントのDOM要素にアクセスすることがあります。
2.2 メディア要素の再生制御
動画や音声プレイヤーのカスタムコンポーネントを作成する際、<video>や<audio>要素のplay()メソッドを親から呼び出す必要があります。
2.3 フォーカス管理
モーダルやドロップダウンのカスタムコンポーネントで、特定のタイミングで要素にフォーカスを当てたい場合に使用します。
2.4 ポップアップやトーストの制御
通知系コンポーネントで、親から「表示する」「非表示にする」といった命令的な操作をしたい場合があります。
3. 実装コード:実務で使える例
では、実際のビジネスロジックに基づいた実装例を見ていきましょう。
3.1 React Hook Formと組み合わせたカスタム入力コンポーネント
ユーザー登録フォームで、バリデーション付きの入力フィールドを作成する例です。
import { forwardRef, InputHTMLAttributes } from 'react';
interface CustomInputProps extends InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
({ label, error, className = '', ...props }, ref) => {
return (
<div className="form-group">
<label className="form-label">{label}</label>
<input
ref={ref}
className={`form-input ${error ? 'border-red-500' : 'border-gray-300'} ${className}`}
{...props}
/>
{error && <span className="text-red-500 text-sm">{error}</span>}
</div>
);
}
);
CustomInput.displayName = 'CustomInput';
export default CustomInput;
このコンポーネントを親から使用する場合:
import { useForm, Controller } from 'react-hook-form';
import CustomInput from './CustomInput';
interface FormData {
email: string;
password: string;
}
function LoginForm() {
const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
defaultValues: {
email: '',
password: ''
}
});
const onSubmit = (data: FormData) => {
console.log('Form submitted:', data);
// APIリクエスト処理など
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="email"
control={control}
rules={{
required: 'メールアドレスは必須です',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '有効なメールアドレスを入力してください'
}
}}
render={({ field }) => (
<CustomInput
{...field}
label="メールアドレス"
type="email"
error={errors.email?.message}
placeholder="user@example.com"
/>
)}
/>
<button type="submit">ログイン</button>
</form>
);
}
3.2 動画プレイヤーのカスタムコンポーネント
実務で動画プレイヤーのカスタマイズが必要な場合があります。
import { forwardRef, useImperativeHandle, useRef } from 'react';
interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (time: number) => void;
getCurrentTime: () => number;
}
interface VideoPlayerProps {
src: string;
autoplay?: boolean;
poster?: string;
}
const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
({ src, autoplay = false, poster }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
getCurrentTime: () => videoRef.current?.currentTime ?? 0
}));
return (
<video
ref={videoRef}
src={src}
autoPlay={autoplay}
poster={poster}
controls
style={{ width: '100%', maxWidth: '800px' }}
/>
);
}
);
VideoPlayer.displayName = 'VideoPlayer';
export default VideoPlayer;
このコンポーネントを使用する親コンポーネント:
import { useRef } from 'react';
import VideoPlayer from './VideoPlayer';
function VideoPlayerPage() {
const videoRef = useRef<VideoPlayerHandle>(null);
const handlePlayButtonClick = () => {
videoRef.current?.play();
};
const handleSkip30Seconds = () => {
const currentTime = videoRef.current?.getCurrentTime() ?? 0;
videoRef.current?.seek(currentTime + 30);
};
return (
<div>
<VideoPlayer
ref={videoRef}
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
/>
<div style={{ marginTop: '20px' }}>
<button onClick={handlePlayButtonClick}>再生</button>
<button onClick={() => videoRef.current?.pause()}>一時停止</button>
<button onClick={handleSkip30Seconds}>30秒スキップ</button>
</div>
</div>
);
}
3.3 トーストメッセージコンポーネント
通知を命令的に表示したい場合の実装:
import { forwardRef, useImperativeHandle, useState } from 'react';
interface ToastHandle {
show: (message: string, type: 'success' | 'error' | 'info') => void;
hide: () => void;
}
const Toast = forwardRef<ToastHandle, {}>((_, ref) => {
const [isVisible, setIsVisible] = useState(false);
const [message, setMessage] = useState('');
const [type, setType] = useState<'success' | 'error' | 'info'>('info');
useImperativeHandle(ref, () => ({
show: (msg: string, msgType: 'success' | 'error' | 'info') => {
setMessage(msg);
setType(msgType);
setIsVisible(true);
// 3秒後に自動的に消える
setTimeout(() => setIsVisible(false), 3000);
},
hide: () => setIsVisible(false)
}));
if (!isVisible) return null;
const bgColor = {
success: 'bg-green-500',
error: 'bg-red-500',
info: 'bg-blue-500'
}[type];
return (
<div className={`${bgColor} text-white px-4 py-3 rounded fixed bottom-4 right-4`}>
{message}
</div>
);
});
Toast.displayName = 'Toast';
export default Toast;
使用例:
import { useRef } from 'react';
import Toast from './Toast';
function DataFormPage() {
const toastRef = useRef<ToastHandle>(null);
const handleFormSubmit = async () => {
try {
// API処理
await submitForm();
toastRef.current?.show('データを保存しました', 'success');
} catch (error) {
toastRef.current?.show('エラーが発生しました', 'error');
}
};
return (
<>
<form onSubmit={(e) => { e.preventDefault(); handleFormSubmit(); }}>
{/* フォーム内容 */}
<button type="submit">送信</button>
</form>
<Toast ref={toastRef} />
</>
);
}
4. よくある応用パターン
4.1 useImperativeHandleとの組み合わせ
forwardRefでDOM要素そのものではなく、カスタムメソッドを公開したい場合は、useImperativeHandleと組み合わせます。前述の動画プレイヤーの例がこれに該当します。
4.2 複数のrefを転送する
複数のDOM要素を親に公開したい場合:
interface MultiElementHandle {
inputRef: HTMLInputElement | null;
buttonRef: HTMLButtonElement | null;
}
const MultiElementComponent = forwardRef<MultiElementHandle, {}>((_, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => ({
inputRef: inputRef.current,
buttonRef: buttonRef.current
}));
return (
<>
<input ref={inputRef} />
<button ref={buttonRef}>Click</button>
</>
);
});
4.3 条件付きでrefを転送する
特定の条件でのみrefの転送をしたい場合:
const ConditionalRefComponent = forwardRef<HTMLDivElement, { shouldForwardRef: boolean }>(
({ shouldForwardRef }, ref) => {
const internalRef = useRef<HTMLDivElement>(null);
return (
<div ref={shouldForwardRef ? ref : internalRef}>
Content
</div>
);
}
);
5. 実務での注意点
5.1 displayNameを必ず設定する
forwardRefでラップされたコンポーネントは、Reactのデベロッパーツールで「Anonymous」と表示されてしまいます。デバッグのしやすさのため、必ずdisplayNameを設定してください。
const MyComponent = forwardRef((props, ref) => {
return <div ref={ref}>Content</div>;
});
MyComponent.displayName = 'MyComponent'; // これが必須
5.2 ref は通常のpropsではない
親コンポーネントからrefを渡す際、通常のpropsとしてアクセスすることはできません。forwardRefでラップされたコンポーネントのみが、refを受け取れます。
5.3 過度な使用を避ける
forwardRefは便利ですが、責務が曖昧になるため、むやみに使いすぎるべきではありません。まずはpropsやコールバックで解決できないか検討してください。
5.4 TypeScriptでの型安全性
TypeScriptを使用している場合、refの型を正確に指定することで、不正なメソッドへのアクセスを事前に防げます。
// ❌ 悪い例:型を指定していない
const Component = forwardRef((props, ref: any) => {});
// ✅ 良い例:型を明確に指定
const Component = forwardRef<HTMLInputElement, Props>((props, ref) => {});
5.5 メモリリークの防止
useImperativeHandleでタイマーやイベントリスナーを設定する場合、クリーンアップ関数を必ず実装してください:
useImperativeHandle(ref, () => ({
startTimer: () => {
const timerId = setInterval(() => {
// 何か処理
}, 1000);
return () => clearInterval(timerId);
}
}));
6. TypeScript を使った型定義のベストプラクティス
大規模プロジェクトでは、ref の型を再利用できるようにしておくと便利です:
// types/components.ts
export interface CustomInputHandle extends HTMLInputElement {}
export interface VideoPlayerHandle {
play: () => void;
pause: () => void;
seek: (time: number) => void;
getCurrentTime: () => number;
getDuration: () => number;
}
export interface ModalHandle {
open: (content: React.ReactNode) => void;
close: () => void;
isOpen: () => boolean;
}
// components/VideoPlayer.tsx
import { VideoPlayerHandle } from '../types/components';
const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>((props, ref) => {
// 実装
});
7. まとめ
React の forwardRef は、カスタムコンポーネントから DOM 要素に直接アクセスが必要な場合に非常に有効なツールです。本記事で解説した実務的なユースケースは以下の通りです:
- フォーム入力管理:React Hook Form との組み合わせで、バリデーション付き入力フィールドを構築
- メディア制御:動画・音声プレイヤーの再生、停止、シーク機能を親から制御
- フォーカス管理:特定のタイミングで要素にフォーカスを当てる
- 命令的 UI 制御:トーストメッセージやモーダルなど、親から制御したいコンポーネント
実装する際の重要なポイントは:
- TypeScript で型を正確に指定し、型安全性を確保する
- displayName を必ず設定してデバッグしやすくする
- useImperativeHandle でカスタムメソッドを公開し、実装の詳細を隠蔽する
- 過度に使用しすぎず、必要な場面に限定して利用する
- メモリリークやクリーンアップを正しく実装する
forwardRef は「最後の手段」という位置付けで使うのが大原則です。まずはコンポーネント間の通常の props やコールバック、Context API などで解決できないか検討し、どうしても必要な場合に限って使用することが、保守性の高いコードにつながります。

