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>
);
});