エラーの内容
カスタムコンポーネントに ref をアタッチしたところ、コンソールに以下のメッセージが表示されました:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
コードはおそらく次のような形になっているはずです:
function TextInput({ placeholder }) {
return <input placeholder={placeholder} />;
}
// どこか別の場所で
const inputRef = useRef(null);
<TextInput ref={inputRef} placeholder="Type here" />
ref は何も動作しません。inputRef.current は null のままで、React はその警告をログに出力しますが、目立ったエラーは発生しません。そのため、このバグは見逃しやすいのです。
なぜこのエラーが発生するのか
ref は React における通常の prop ではありません。関数コンポーネントに ref={inputRef} と記述しても、React はコンポーネントに渡される前にそれを横取りします。つまり props には一切現れません。関数コンポーネントにはインスタンスが存在しないため、ref をアタッチする場所がなく、React は ref を完全に破棄してしまいます。
クラスコンポーネントはインスタンスを持つため、この問題を回避できます。関数コンポーネントはそうではありません。だからこそ React.forwardRef() が存在します。これは明示的なオプトインであり、「このコンポーネントは ref の扱い方を知っている」と React に伝えるものです。
修正方法:React.forwardRef でラップする
コンポーネントを forwardRef でラップします。すると React は ref を props とは別に、関数の第2引数として渡します。あとは公開したい DOM 要素にそれをアタッチするだけです。
修正前(動作しない)
function TextInput({ placeholder }) {
return <input placeholder={placeholder} />;
}
修正後(動作する)
import { forwardRef } from 'react';
const TextInput = forwardRef(function TextInput({ placeholder }, ref) {
return <input ref={ref} placeholder={placeholder} />;
});
export default TextInput;
親コンポーネントから直接、内部の <input> DOM ノードにアクセスできるようになります:
import { useRef } from 'react';
import TextInput from './TextInput';
function Form() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current?.focus();
}
return (
<>
<TextInput ref={inputRef} placeholder="Type here" />
<button onClick={focusInput}>Focus</button>
</>
);
}
アロー関数を使う場合(こちらも有効)
const TextInput = forwardRef(({ placeholder }, ref) => (
<input ref={ref} placeholder={placeholder} />
));
TextInput.displayName = 'TextInput';
アロー関数コンポーネントには必ず displayName を設定してください。設定しないと、React DevTools でコンポーネントが ForwardRef と表示されます。1つなら問題ありませんが、同じものが5つ積み重なったコンポーネントツリーをデバッグする羽目になると困ります。
ネストされた要素への ref の転送
ref は必ずしもルート要素に設定する必要はありません。呼び出し元が実際にアクセスしたい場所に配置してください:
const Card = forwardRef(function Card({ children, className }, ref) {
return (
<div className="card-wrapper">
<div ref={ref} className={`card ${className}`}>
{children}
</div>
</div>
);
});
TypeScript:forwardRef の正しい型付け
ジェネリックのシグネチャは forwardRef<RefType, PropsType> です。どちらの型パラメータも重要です:
import { forwardRef, InputHTMLAttributes } from 'react';
interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
}
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
function TextInput({ label, ...props }, ref) {
return (
<div>
{label && <label>{label}</label>}
<input ref={ref} {...props} />
</div>
);
}
);
export default TextInput;
ジェネリクスを省略すると、TypeScript は ref を unknown として推論します。その結果、ref を使用する箇所で型エラーが発生します。しかもそれがまったく別のファイルで起きることも多く、根本原因を特定するのが面倒になります。
よくある間違い:ref のアタッチ忘れ
これは多くの人がはまるミスです。forwardRef でラップしてコンソールの警告が消え、一見すべて正常に見えます。しかし ref.current はまだ null のままです:
// バグ:ref 引数は受け取っているが使用していない
const TextInput = forwardRef(function TextInput({ placeholder }, ref) {
return <input placeholder={placeholder} />; // ref がアタッチされていない!
});
引数として ref を受け取ることは、作業の半分に過ぎません。実際に機能させるためには、DOM 要素または別の forwardRef コンポーネントにそれを渡す必要があります。
修正が正しく適用されたか確認する
- ブラウザのコンソールから警告が消えている。
- マウント後に
inputRef.currentがnullでなくなっている。簡単なuseEffectログで確認できます:
useEffect(() => {
console.log('ref:', inputRef.current); // DOM 要素が表示されるはず(例:<input>)
}, []);
- React DevTools でコンポーネントが名前付きで表示される(
ForwardRefではなくTextInput)。これでdisplayNameが設定されていることが確認できます。 .focus()や.scrollIntoView()といった命令的な呼び出しがエラーなく動作する。
クイックリファレンス
- 通常の関数コンポーネント +
refprop → 警告が出て ref はnull forwardRefでラップする → 第2引数が ref になるので、DOM ノードにアタッチする- TypeScript:
forwardRef<HTMLElement, Props>ジェネリクスを使用する(どちらも重要) - アロー関数コンポーネントには DevTools で読みやすくなるよう
displayNameを設定する - 警告は消えたのに ref がまだ
null?ラップは正しいが、内部で ref のアタッチを忘れている

