React forwardRefの使い方|コンポーネント間の参照を理解する
React forwardRefの基本から応用まで詳しく解説。ref の転送、カスタムフック、TypeScript対応など実践的な使い方を具体例とともに紹介します。
みなさん、Reactで開発していてこんな悩みを抱えたことはありませんか?
「カスタムコンポーネントにrefを渡したけど動かない」
「子コンポーネントの要素に直接アクセスしたい」
「forwardRefって何?どう使うの?」
カスタムコンポーネントでのref転送は、Reactの中でも少し複雑な部分です。 でも理解できれば、より柔軟で使いやすいコンポーネントが作れるようになります。
この記事では、React forwardRefの基本から実践的な使い方まで詳しく解説します。 最初は難しく感じるかもしれませんが、一緒に学んでいきましょう。
forwardRefって何?基本を理解しよう
まずは、なぜforwardRefが必要なのかを理解しましょう。 普通のrefと何が違うのかがわかると、使い方もすっきりと理解できますよ。
普通のrefの使い方
最初に、通常のrefの使い方を確認してみましょう。
import React, { useRef, useEffect } from 'react';
function BasicExample() {
const inputRef = useRef(null);
useEffect(() => {
// ページ読み込み時にinputにフォーカス
inputRef.current.focus();
}, []);
const handleClick = () => {
// ボタンクリック時に入力をクリア
inputRef.current.value = '';
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} placeholder="ここに入力してください" />
<button onClick={handleClick}>クリア</button>
</div>
);
}
このようにHTML要素に直接refを使うのは、とても簡単ですね。
inputRef.current
で要素にアクセスできます。
カスタムコンポーネントでの問題
ところが、カスタムコンポーネントにrefを渡そうとすると問題が発生します。
// ❌ この方法は動きません
function CustomInput({ placeholder }) {
return <input placeholder={placeholder} />;
}
function App() {
const inputRef = useRef(null);
useEffect(() => {
// エラー:inputRef.current は null
inputRef.current.focus(); // TypeError!
}, []);
return (
<div>
{/* refがカスタムコンポーネントには渡されない */}
<CustomInput ref={inputRef} placeholder="カスタム入力" />
</div>
);
}
このコードを実行すると、エラーが発生します。 なぜなら、カスタムコンポーネントは通常のpropsとしてrefを受け取れないからです。
forwardRefで解決!
forwardRefを使うことで、この問題を解決できます。
import React, { forwardRef, useRef, useEffect } from 'react';
// ✅ forwardRef を使ってref転送
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
function App() {
const inputRef = useRef(null);
useEffect(() => {
// 正常に動作:input要素にアクセス可能
inputRef.current.focus();
}, []);
return (
<div>
<CustomInput ref={inputRef} placeholder="カスタム入力" />
</div>
);
}
forwardRef
でコンポーネントをラップすることで、refを正しく転送できます。
これで、親コンポーネントからカスタムコンポーネント内の要素にアクセスできるようになりました。
forwardRefの基本的な仕組み
forwardRefの構造を詳しく見てみましょう。
// forwardRef の基本構造
const MyComponent = forwardRef((props, ref) => {
// props: 通常のprops
// ref: 親から渡されたref
return <div ref={ref}>コンテンツ</div>;
});
// 使用例
function ParentComponent() {
const myRef = useRef(null);
return <MyComponent ref={myRef} />;
}
forwardRefは第1引数にprops、第2引数にrefを受け取ります。 この順番は決まっているので、間違えないようにしましょう。
実用的なforwardRefの使い方
基本がわかったところで、実際のプロジェクトで使えるパターンを見てみましょう。 どれも実用的で、覚えておくと開発が楽になりますよ。
高機能な入力コンポーネント
まずは、よく使われる入力コンポーネントを作ってみましょう。
import React, { forwardRef, useState } from 'react';
// 高機能な入力コンポーネント
const CustomInput = forwardRef(({
label,
error,
helperText,
required = false,
...props
}, ref) => {
const [isFocused, setIsFocused] = useState(false);
return (
<div className={`form-group ${error ? 'error' : ''} ${isFocused ? 'focused' : ''}`}>
{label && (
<label className="form-label">
{label}
{required && <span className="required">*</span>}
</label>
)}
<input
ref={ref}
className="form-input"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
{...props}
/>
{error && <div className="error-message">{error}</div>}
{helperText && !error && (
<div className="helper-text">{helperText}</div>
)}
</div>
);
});
このコンポーネントには、以下の機能が含まれています。
- ラベル表示:
label
プロップで見出しを設定 - エラー表示:
error
プロップでエラーメッセージを表示 - ヘルプテキスト:
helperText
で説明文を表示 - フォーカス状態: フォーカス時にスタイルが変わる
次に、このコンポーネントを使ったフォームを作ってみましょう。
function ContactForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const messageRef = useRef(null);
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
message: messageRef.current.value
};
// バリデーション
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = '名前は必須です';
nameRef.current.focus(); // エラーフィールドにフォーカス
return;
}
if (!formData.email.trim()) {
newErrors.email = 'メールアドレスは必須です';
emailRef.current.focus();
return;
}
// フォーム送信処理
console.log('送信データ:', formData);
// フォームリセット
nameRef.current.value = '';
emailRef.current.value = '';
messageRef.current.value = '';
nameRef.current.focus();
};
return (
<form onSubmit={handleSubmit} className="contact-form">
<h2>お問い合わせ</h2>
<CustomInput
ref={nameRef}
label="お名前"
placeholder="山田太郎"
required
error={errors.name}
/>
<CustomInput
ref={emailRef}
label="メールアドレス"
type="email"
placeholder="example@email.com"
required
error={errors.email}
helperText="返信用のメールアドレスを入力してください"
/>
<CustomInput
ref={messageRef}
label="メッセージ"
as="textarea"
rows={5}
placeholder="お問い合わせ内容をご記入ください"
required
/>
<button type="submit" className="btn btn-primary">
送信
</button>
</form>
);
}
forwardRefを使うことで、以下のことができるようになりました。
- 直接的な値取得: refを使って入力値を直接取得
- フォーカス制御: エラー時に該当フィールドにフォーカス
- フォームリセット: 送信後に全フィールドをクリア
モーダルコンポーネント
次に、外部から操作できるモーダルコンポーネントを作ってみましょう。
import React, { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
// モーダルコンポーネント
const Modal = forwardRef(({
title,
onClose,
children
}, ref) => {
const [isOpen, setIsOpen] = useState(false);
// 親コンポーネントから呼び出せるメソッドを定義
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
toggle: () => setIsOpen(prev => !prev),
isOpen: () => isOpen
}));
// ESCキーでモーダルを閉じる
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'Escape' && isOpen) {
setIsOpen(false);
onClose?.();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen) return null;
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget) {
setIsOpen(false);
onClose?.();
}
};
return (
<div className="modal-backdrop" onClick={handleBackdropClick}>
<div className="modal-content">
<div className="modal-header">
<h2>{title}</h2>
<button
className="modal-close"
onClick={() => {
setIsOpen(false);
onClose?.();
}}
>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
);
});
useImperativeHandle
を使うことで、親から呼び出せるメソッドを定義できます。
- open(): モーダルを開く
- close(): モーダルを閉じる
- toggle(): 開閉状態を切り替え
- isOpen(): 現在の状態を取得
このモーダルの使用例を見てみましょう。
function App() {
const modalRef = useRef(null);
const handleDeleteClick = () => {
modalRef.current.open();
};
return (
<div className="app">
<h1>モーダル使用例</h1>
<button
onClick={() => modalRef.current.open()}
className="btn btn-primary"
>
モーダルを開く
</button>
<Modal
ref={modalRef}
title="サンプルモーダル"
onClose={() => console.log('モーダルが閉じられました')}
>
<p>これはモーダルの内容です。</p>
<p>ESCキーで閉じることもできます。</p>
</Modal>
</div>
);
}
このようにrefを使うことで、ボタンクリック時にモーダルを開くことができます。 stateを親で管理する必要がなく、コンポーネントがより独立性を保てます。
アニメーション対応ボタン
最後に、アニメーション機能付きのボタンコンポーネントを作ってみましょう。
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
// アニメーション機能付きボタン
const AnimatedButton = forwardRef(({
children,
onClick,
loading = false,
variant = 'primary',
...props
}, ref) => {
const buttonRef = useRef(null);
const [isAnimating, setIsAnimating] = useState(false);
// 親から呼び出せるメソッド
useImperativeHandle(ref, () => ({
focus: () => buttonRef.current?.focus(),
blur: () => buttonRef.current?.blur(),
click: () => buttonRef.current?.click(),
startPulse: () => setIsAnimating(true),
stopPulse: () => setIsAnimating(false),
getElement: () => buttonRef.current
}));
const handleClick = async (e) => {
if (loading) return;
// クリックアニメーション
const button = buttonRef.current;
button.style.transform = 'scale(0.95)';
setTimeout(() => {
button.style.transform = 'scale(1)';
}, 150);
// 親のonClickを実行
if (onClick) {
await onClick(e);
}
};
return (
<button
ref={buttonRef}
className={`animated-btn animated-btn--${variant} ${isAnimating ? 'pulse' : ''}`}
onClick={handleClick}
disabled={loading}
{...props}
>
{loading ? (
<span className="loading-spinner">⏳</span>
) : (
children
)}
</button>
);
});
このボタンは以下の機能を持っています。
- クリックアニメーション: クリック時に縮小→拡大
- パルスアニメーション: 外部から開始・停止可能
- ローディング状態: 処理中の表示
- フォーカス制御: 外部からフォーカス操作
使用例を見てみましょう。
function ButtonDemo() {
const primaryBtnRef = useRef(null);
const successBtnRef = useRef(null);
const [loading, setLoading] = useState(false);
const handleSave = async () => {
setLoading(true);
try {
// 保存処理をシミュレート
await new Promise(resolve => setTimeout(resolve, 2000));
// 成功時のアニメーション
successBtnRef.current.startPulse();
setTimeout(() => {
successBtnRef.current.stopPulse();
}, 1000);
console.log('保存が完了しました');
} catch (error) {
console.error('保存に失敗しました:', error);
} finally {
setLoading(false);
}
};
return (
<div className="button-demo">
<h2>アニメーションボタンデモ</h2>
<div className="button-group">
<AnimatedButton
ref={primaryBtnRef}
variant="primary"
onClick={handleSave}
loading={loading}
>
{loading ? '保存中...' : '保存'}
</AnimatedButton>
<AnimatedButton
ref={successBtnRef}
variant="success"
onClick={() => console.log('成功ボタンがクリックされました')}
>
成功
</AnimatedButton>
</div>
<div className="controls">
<button
onClick={() => primaryBtnRef.current.startPulse()}
className="btn btn-secondary"
>
プライマリボタンをパルス
</button>
<button
onClick={() => {
primaryBtnRef.current.stopPulse();
successBtnRef.current.stopPulse();
}}
className="btn btn-secondary"
>
全アニメーション停止
</button>
</div>
</div>
);
}
このように、forwardRefとuseImperativeHandleを組み合わせることで、非常に柔軟なコンポーネントが作れます。
useImperativeHandleの活用法
useImperativeHandleは、forwardRefと組み合わせて使う重要なフックです。 親コンポーネントから呼び出せるメソッドを自由に定義できます。
基本的な使い方
シンプルなカウンターコンポーネントで使い方を見てみましょう。
import React, { forwardRef, useImperativeHandle, useState } from 'react';
// カウンターコンポーネント
const Counter = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
// 親コンポーネントから呼び出せるメソッドを定義
useImperativeHandle(ref, () => ({
// カウンターをリセット
reset: () => setCount(0),
// 指定した値に設定
setValue: (value) => setCount(value),
// 現在の値を取得
getValue: () => count,
// 値を増加
increment: (step = 1) => setCount(prev => prev + step),
// 値を減少
decrement: (step = 1) => setCount(prev => prev - step),
// 値が特定の条件を満たすかチェック
isEven: () => count % 2 === 0,
isPositive: () => count > 0
}));
return (
<div className="counter">
<h3>カウンター: {count}</h3>
<div className="counter-controls">
<button onClick={() => setCount(prev => prev - 1)}>-1</button>
<button onClick={() => setCount(prev => prev + 1)}>+1</button>
</div>
</div>
);
});
このカウンターコンポーネントでは、以下のメソッドを外部に公開しています。
- reset(): カウンターを0にリセット
- setValue(value): 指定した値に設定
- getValue(): 現在の値を取得
- increment(step): 指定した分だけ増加
- decrement(step): 指定した分だけ減少
- isEven(): 偶数かどうかを判定
- isPositive(): 正の数かどうかを判定
使用例を見てみましょう。
function CounterApp() {
const counter1Ref = useRef(null);
const counter2Ref = useRef(null);
const handleReset = () => {
counter1Ref.current.reset();
counter2Ref.current.reset();
};
const handleRandomValues = () => {
const randomValue1 = Math.floor(Math.random() * 100);
const randomValue2 = Math.floor(Math.random() * 100);
counter1Ref.current.setValue(randomValue1);
counter2Ref.current.setValue(randomValue2);
};
const handleGetInfo = () => {
const value1 = counter1Ref.current.getValue();
const value2 = counter2Ref.current.getValue();
const info = {
counter1: {
value: value1,
isEven: counter1Ref.current.isEven(),
isPositive: counter1Ref.current.isPositive()
},
counter2: {
value: value2,
isEven: counter2Ref.current.isEven(),
isPositive: counter2Ref.current.isPositive()
},
sum: value1 + value2
};
console.log('カウンター情報:', info);
alert(`合計: ${info.sum}`);
};
return (
<div className="counter-app">
<h2>カウンター管理アプリ</h2>
<div className="counters">
<Counter ref={counter1Ref} />
<Counter ref={counter2Ref} />
</div>
<div className="app-controls">
<button onClick={handleReset} className="btn btn-secondary">
全リセット
</button>
<button onClick={handleRandomValues} className="btn btn-primary">
ランダム値設定
</button>
<button onClick={handleGetInfo} className="btn btn-info">
情報取得
</button>
<button
onClick={() => {
counter1Ref.current.increment(5);
counter2Ref.current.decrement(3);
}}
className="btn btn-success"
>
カウンター1を+5、カウンター2を-3
</button>
</div>
</div>
);
}
useImperativeHandleを使うことで、コンポーネント内部の状態や機能を外部から制御できるようになります。
フォームバリデーションの実例
より実用的な例として、バリデーション機能付きのフォームフィールドを作ってみましょう。
import React, { forwardRef, useImperativeHandle, useState, useRef } from 'react';
// バリデーション機能付きフォームフィールド
const ValidatedField = forwardRef(({
label,
type = 'text',
rules = [],
...props
}, ref) => {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const [touched, setTouched] = useState(false);
const inputRef = useRef(null);
// バリデーション実行
const validate = (val = value) => {
for (const rule of rules) {
const result = rule(val);
if (result !== true) {
setError(result);
return false;
}
}
setError('');
return true;
};
// 親から呼び出せるメソッド
useImperativeHandle(ref, () => ({
getValue: () => value,
setValue: (val) => {
setValue(val);
if (touched) validate(val);
},
validate: () => validate(),
focus: () => inputRef.current?.focus(),
clear: () => {
setValue('');
setError('');
setTouched(false);
},
isValid: () => !error && touched,
hasError: () => !!error,
getError: () => error
}));
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
if (touched) {
validate(newValue);
}
};
const handleBlur = () => {
setTouched(true);
validate();
};
return (
<div className={`form-field ${error ? 'error' : ''} ${touched && !error ? 'valid' : ''}`}>
<label>{label}</label>
<input
ref={inputRef}
type={type}
value={value}
onChange={handleChange}
onBlur={handleBlur}
{...props}
/>
{error && <div className="error-message">{error}</div>}
</div>
);
});
このコンポーネントは以下のメソッドを公開しています。
- getValue(): 現在の入力値を取得
- setValue(val): 値を設定
- validate(): バリデーションを実行
- focus(): フィールドにフォーカス
- clear(): フィールドをクリア
- isValid(): バリデーション状態をチェック
- hasError(): エラーがあるかチェック
- getError(): エラーメッセージを取得
バリデーションルールも定義してみましょう。
// バリデーションルール
const validationRules = {
required: (value) => value.trim() ? true : 'この項目は必須です',
minLength: (min) => (value) =>
value.length >= min ? true : `${min}文字以上で入力してください`,
maxLength: (max) => (value) =>
value.length <= max ? true : `${max}文字以下で入力してください`,
email: (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value) ? true : '有効なメールアドレスを入力してください';
}
};
これらを使った登録フォームの例です。
function RegistrationForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);
const passwordRef = useRef(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
// 全フィールドのバリデーション
const fields = [nameRef, emailRef, passwordRef];
const isValid = fields.every(fieldRef => fieldRef.current.validate());
if (!isValid) {
// 最初のエラーフィールドにフォーカス
const firstErrorField = fields.find(fieldRef => fieldRef.current.hasError());
if (firstErrorField) {
firstErrorField.current.focus();
}
return;
}
// フォーム送信
setIsSubmitting(true);
try {
const formData = {
name: nameRef.current.getValue(),
email: emailRef.current.getValue(),
password: passwordRef.current.getValue()
};
console.log('送信データ:', formData);
// API送信をシミュレート
await new Promise(resolve => setTimeout(resolve, 2000));
alert('登録が完了しました!');
// フォームクリア
fields.forEach(fieldRef => fieldRef.current.clear());
nameRef.current.focus();
} catch (error) {
alert('登録に失敗しました');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="registration-form">
<h2>ユーザー登録</h2>
<ValidatedField
ref={nameRef}
label="お名前"
placeholder="山田太郎"
rules={[
validationRules.required,
validationRules.minLength(2),
validationRules.maxLength(50)
]}
/>
<ValidatedField
ref={emailRef}
label="メールアドレス"
type="email"
placeholder="example@email.com"
rules={[
validationRules.required,
validationRules.email
]}
/>
<ValidatedField
ref={passwordRef}
label="パスワード"
type="password"
placeholder="8文字以上で入力"
rules={[
validationRules.required,
validationRules.minLength(8)
]}
/>
<div className="form-actions">
<button
type="submit"
className="btn btn-primary"
disabled={isSubmitting}
>
{isSubmitting ? '登録中...' : '登録'}
</button>
</div>
</form>
);
}
このように、useImperativeHandleを使うことで非常に柔軟なフォーム処理が実現できます。
TypeScriptでのforwardRef
TypeScriptを使っている場合の、forwardRefの型定義方法を見てみましょう。 正しく型を定義することで、より安全にコンポーネントを使えます。
基本的な型定義
まず、シンプルなボタンコンポーネントの型定義から見てみましょう。
import React, { forwardRef } from 'react';
// props の型定義
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
}
// forwardRef with TypeScript
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'medium', loading = false, children, ...props }, ref) => {
return (
<button
ref={ref}
className={`btn btn--${variant} btn--${size} ${loading ? 'loading' : ''}`}
disabled={loading}
{...props}
>
{loading ? 'Loading...' : children}
</button>
);
}
);
Button.displayName = 'Button';
TypeScriptでforwardRefを使う時のポイントは以下の通りです。
- 第1ジェネリック: ref の型(HTMLButtonElement)
- 第2ジェネリック: props の型(ButtonProps)
- displayName: React DevToolsでの表示名
useImperativeHandleの型定義
useImperativeHandleを使う場合の型定義方法を見てみましょう。
import React, {
forwardRef,
useImperativeHandle,
useState
} from 'react';
// 公開するメソッドの型定義
interface CounterHandle {
getValue: () => number;
setValue: (value: number) => void;
increment: (step?: number) => void;
decrement: (step?: number) => void;
reset: () => void;
}
interface CounterProps {
initialValue?: number;
min?: number;
max?: number;
onValueChange?: (value: number) => void;
}
const Counter = forwardRef<CounterHandle, CounterProps>(
({ initialValue = 0, min, max, onValueChange }, ref) => {
const [count, setCount] = useState(initialValue);
const updateCount = (newValue: number) => {
let clampedValue = newValue;
if (min !== undefined && clampedValue < min) {
clampedValue = min;
}
if (max !== undefined && clampedValue > max) {
clampedValue = max;
}
setCount(clampedValue);
onValueChange?.(clampedValue);
};
useImperativeHandle(ref, () => ({
getValue: () => count,
setValue: (value: number) => updateCount(value),
increment: (step: number = 1) => updateCount(count + step),
decrement: (step: number = 1) => updateCount(count - step),
reset: () => updateCount(initialValue)
}));
return (
<div className="counter">
<span>Count: {count}</span>
<button onClick={() => updateCount(count - 1)}>-</button>
<button onClick={() => updateCount(count + 1)}>+</button>
</div>
);
}
);
Counter.displayName = 'Counter';
使用例はこんな感じになります。
function CounterApp() {
const counterRef = useRef<CounterHandle>(null);
const handleReset = () => {
counterRef.current?.reset();
};
const handleSetRandom = () => {
const randomValue = Math.floor(Math.random() * 100);
counterRef.current?.setValue(randomValue);
};
return (
<div>
<Counter
ref={counterRef}
initialValue={10}
min={0}
max={100}
onValueChange={(value) => console.log('Value changed:', value)}
/>
<button onClick={handleReset}>Reset</button>
<button onClick={handleSetRandom}>Random</button>
</div>
);
}
TypeScriptを使うことで、メソッドの引数や戻り値の型が保証され、より安全にコンポーネントを使えるようになります。
よくある間違いと対処法
forwardRefを使う時によくある間違いと、その解決方法をご紹介します。 事前に知っておくことで、スムーズに開発を進められますよ。
forwardRefを忘れてしまう
これは最もよくある間違いです。
// ❌ forwardRef を使用していない
function CustomInput({ placeholder }, ref) {
return <input ref={ref} placeholder={placeholder} />;
}
// 使用時に警告が出る
function App() {
const inputRef = useRef(null);
return <CustomInput ref={inputRef} placeholder="入力" />; // Warning!
}
この場合、以下のような警告が表示されます。
Warning: Function components cannot be given refs.
Attempts to access this ref will fail.
解決方法
// ✅ forwardRef を使用
const CustomInput = forwardRef(({ placeholder }, ref) => {
return <input ref={ref} placeholder={placeholder} />;
});
forwardRefでコンポーネントをラップすることで、正しくrefが転送されます。
useImperativeHandleの依存配列を忘れる
useImperativeHandleでは、依存配列の指定が重要です。
// ❌ 依存配列が不適切
const Counter = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
getValue: () => count, // count が古い値を参照する可能性
increment: () => setCount(count + 1)
})); // 依存配列がない
return <div>{count}</div>;
});
この場合、count
が古い値を参照してしまう可能性があります。
解決方法
// ✅ 適切な依存配列と関数の使用
const Counter = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
useImperativeHandle(ref, () => ({
getValue: () => count,
increment: () => setCount(prev => prev + 1) // 関数形式を使用
}), [count]); // count を依存配列に含める
return <div>{count}</div>;
});
依存配列を正しく指定し、必要に応じて関数形式のsetStateを使いましょう。
displayNameの設定を忘れる
React DevToolsでの表示が分かりにくくなります。
// ❌ displayName が設定されていない
const CustomButton = forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
// React DevTools で "ForwardRef" と表示される
解決方法
// ✅ displayName を設定
const CustomButton = forwardRef((props, ref) => {
return <button ref={ref} {...props} />;
});
CustomButton.displayName = 'CustomButton';
displayNameを設定することで、React DevToolsで見つけやすくなります。
まとめ:forwardRefをマスターしよう!
React forwardRefについて、基本から実践的な使い方まで詳しく解説しました。
重要なポイント
- forwardRefの必要性: カスタムコンポーネントでrefを使うために必要
- 基本的な使い方: propsとrefを受け取る構造
- useImperativeHandle: 公開するメソッドを自由に定義
- TypeScript対応: 適切な型定義で安全性を向上
活用できる場面
- フォーム操作: 入力フィールドへの直接アクセス
- アニメーション制御: 外部からアニメーションを操作
- モーダル管理: 開閉状態を外部から制御
- フォーカス制御: エラー時の自動フォーカス
実装時のコツ
- 必要な機能のみ公開: useImperativeHandleで適切なメソッドを選択
- 型安全性の確保: TypeScriptでの型定義
- 依存配列の管理: useImperativeHandleの正しい依存関係
- デバッグしやすさ: displayNameの設定
forwardRefをマスターすることで、より柔軟で再利用可能なReactコンポーネントが作れるようになります。 最初は複雑に感じるかもしれませんが、一度覚えてしまえば非常に強力な機能です。
ぜひ実際のプロジェクトで活用してみてください。 きっと、より使いやすいコンポーネントライブラリが作れるようになりますよ!