forwardRef
forwardRef
は、親コンポーネントに対して DOM ノードを ref として公開できるようにします。
const SomeComponent = forwardRef(render)
リファレンス
forwardRef(render)
forwardRef()
を呼び出すことで、コンポーネントが ref を受け取ってそれを子コンポーネントに転送 (forward) できるようになります。
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
// ...
});
引数
render
: コンポーネントのレンダー関数です。React はこの関数を親から受け取った props およびref
とともに呼び出します。返す JSX がコンポーネントの出力となります。
返り値
forwardRef
は JSX でレンダーできる React コンポーネントを返します。プレーンな関数として定義された React コンポーネントとは異なり、forwardRef
によって返されるコンポーネントは ref
属性を受け取ることもできます。
注意点
- Strict Mode では、React はレンダー関数が誤って純関数でなくなってしまう問題を見つけやすくするため、レンダー関数を 2 回呼び出します。これは開発環境専用の挙動であり、本番環境には影響しません。レンダー関数が純粋である場合(そうであるべきです)、これはコンポーネントのロジックに影響を与えません。呼び出しのうちの一方からの結果は無視されます。
render
関数
forwardRef
は引数としてレンダー関数を受け取ります。React はこの関数を props
および ref
とともに呼び出します。
const MyInput = forwardRef(function MyInput(props, ref) {
return (
<label>
{props.label}
<input ref={ref} />
</label>
);
});
引数
-
props
: 親コンポーネントから渡された props です。 -
ref
: 親コンポーネントから渡されたref
属性です。ref
はオブジェクトの場合と関数の場合があります。親コンポーネントが ref を渡していない場合はnull
になります。受け取ったref
は、別のコンポーネントに渡すか、useImperativeHandle
に渡します。
返り値
forwardRef
は JSX でレンダーできる React コンポーネントを返します。プレーンな関数として定義された React コンポーネントとは異なり、forwardRef
によって返されるコンポーネントは ref
属性を受け取ることができます。
使用法
親コンポーネントに DOM ノードを公開する
デフォルトでは、各コンポーネント内の DOM ノードはプライベートです。しかし、時には親に DOM ノードを公開することが有用な場合があります。例えば、ノードにフォーカスを当てることを許可したい場合です。これを明示的に許可するために、コンポーネント定義を forwardRef()
でラップします。
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const { label, ...otherProps } = props;
return (
<label>
{label}
<input {...otherProps} />
</label>
);
});
props の後の第 2 引数として ref が渡されます。公開したい DOM ノードにそれを渡してください。
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const { label, ...otherProps } = props;
return (
<label>
{label}
<input {...otherProps} ref={ref} />
</label>
);
});
これで、親の Form
コンポーネントが、MyInput
によって公開された <input>
DOM ノードにアクセスできるようになります。
function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
}
return (
<form>
<MyInput label="Enter your name:" ref={ref} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
この Form
コンポーネントは MyInput
に ref を渡しています。MyInput
コンポーネントはその ref をブラウザの <input>
タグに転送しています。その結果、Form
コンポーネントはこの <input>
DOM ノードにアクセスし、focus()
を呼び出すことができるようになります。
コンポーネント内の DOM ノードへの ref を公開することで、後でコンポーネントの内部を変更するのが難しくなることに注意してください。通常は、ボタンやテキスト入力フィールドなどの再利用可能な低レベルコンポーネントからは DOM ノードの公開を行いますが、アバターやコメントのようなアプリケーションレベルのコンポーネントでは行いません。
例 1/2: テキスト入力フィールドにフォーカス
ボタンをクリックすると、入力フィールドにフォーカスが当てられます。Form
コンポーネントは ref を定義し、それを MyInput
コンポーネントに渡します。MyInput
コンポーネントはその ref をブラウザの <input>
に転送します。これにより、Form
コンポーネントは <input>
にフォーカスを当てられるようになります。
import { useRef } from 'react'; import MyInput from './MyInput.js'; export default function Form() { const ref = useRef(null); function handleClick() { ref.current.focus(); } return ( <form> <MyInput label="Enter your name:" ref={ref} /> <button type="button" onClick={handleClick}> Edit </button> </form> ); }
複数コンポーネントを経由した ref の転送
ref
を DOM ノードに転送する代わりに、独自コンポーネントである MyInput
に転送することもできます。
const FormField = forwardRef(function FormField(props, ref) {
// ...
return (
<>
<MyInput ref={ref} />
...
</>
);
});
さらにその MyInput
コンポーネントが自身の <input>
に ref を転送すれば、FormField
への ref はその <input>
への参照を受け取ることになります。
function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
}
return (
<form>
<FormField label="Enter your name:" ref={ref} isRequired={true} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
Form
コンポーネントは ref を定義し、それを FormField
に渡しています。FormField
コンポーネントはその ref を MyInput
に転送し、MyInput
はそれをブラウザの <input>
DOM ノードに転送しています。これで Form
が DOM ノードにアクセスできるようになります。
import { useRef } from 'react'; import FormField from './FormField.js'; export default function Form() { const ref = useRef(null); function handleClick() { ref.current.focus(); } return ( <form> <FormField label="Enter your name:" ref={ref} isRequired={true} /> <button type="button" onClick={handleClick}> Edit </button> </form> ); }
DOM ノードの代わりに命令型ハンドルを公開する
DOM ノードをまるごと公開する代わりに、使用できるメソッドを制限したカスタムオブジェクトである、命令型ハンドル (imperative handle) を公開することができます。これを行うには、DOM ノードを保持するための別の ref を定義します。
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
// ...
return <input {...props} ref={inputRef} />;
});
そして受け取った ref
を useImperativeHandle
に渡し、ref
で公開したい値を指定します。
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
});
何らかのコンポーネントが MyInput
への ref を取得すると、DOM ノードの代わりにあなたが書いた { focus, scrollIntoView }
というオブジェクトを受け取ります。これにより、DOM ノードについて公開する情報を最小限に制限することができます。
import { useRef } from 'react'; import MyInput from './MyInput.js'; export default function Form() { const ref = useRef(null); function handleClick() { ref.current.focus(); // This won't work because the DOM node isn't exposed: // ref.current.style.opacity = 0.5; } return ( <form> <MyInput label="Enter your name:" ref={ref} /> <button type="button" onClick={handleClick}> Edit </button> </form> ); }
トラブルシューティング
コンポーネントを forwardRef
でラップしているのに、ref
が常に null
になる
これは通常、受け取った ref
を実際に使用するのを忘れていることを意味します。
例えば、このコンポーネントは ref
を全く使用していません:
const MyInput = forwardRef(function MyInput({ label }, ref) {
return (
<label>
{label}
<input />
</label>
);
});
修正するにはこの ref
を、DOM ノードか、ref を受け入れることができる別のコンポーネントに渡します。
const MyInput = forwardRef(function MyInput({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
});
一部のロジックが条件付きである場合にも、MyInput
への ref
が null
になることがあります。
const MyInput = forwardRef(function MyInput({ label, showInput }, ref) {
return (
<label>
{label}
{showInput && <input ref={ref} />}
</label>
);
});
showInput
が false
の場合、ref はどのノードにも転送されないため、MyInput
への ref は空のままになります。特に、以下の例のように条件が別のコンポーネント、例えば Panel
の中に隠されている場合、これを見落としがちです。
const MyInput = forwardRef(function MyInput({ label, showInput }, ref) {
return (
<label>
{label}
<Panel isExpanded={showInput}>
<input ref={ref} />
</Panel>
</label>
);
});