forwardRef
forwardRef 让你的组件可以通过 ref. 向父组件暴露一个 DOM 节点
const SomeComponent = forwardRef(render)参考
forwardRef(render)
调用 forwardRef(),让你的组件接收一个 ref,并将它转发给子组件:
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
// ...
});参数
render:你组件的渲染函数。React 会使用你的组件从父组件接收到的 props 和ref调用这个函数。你返回的 JSX 将成为你组件的输出。
返回值
forwardRef 返回一个可以在 JSX 中渲染的 React 组件。与普通函数定义的 React 组件不同,forwardRef 返回的组件也能够接收 ref prop。
注意事项
- 在严格模式下,React 会调用你的渲染函数两次,以帮助你发现意外的副作用。这是仅在开发环境中的行为,不会影响生产环境。如果你的渲染函数是纯函数(它应该如此),这不会影响你组件的逻辑。两次调用中的一次结果会被忽略。
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 prop。
用法
向父组件暴露一个 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 之后收到第二个参数 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 组件传递了一个 ref 给 MyInput。MyInput 组件将这个 ref 转发 到 <input> 浏览器标签。结果,Form 组件可以访问那个 <input> DOM 节点,并对它调用 focus()。
请记住,将 ref 暴露给组件内部的 DOM 节点,会让你以后更难修改组件内部实现。通常你会从可复用的底层组件中暴露 DOM 节点,比如按钮或文本输入框,但不会对头像或评论这类应用层组件这样做。
Example 1 of 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 组件将 ref 转发到它的 <input> 上,那么指向 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 节点,而是暴露一个自定义对象,称为 命令式句柄,并提供一组更受限制的方法。为此,你需要定义一个单独的 ref 来保存 DOM 节点:
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,它接收到的将只是你的 { focus, scrollIntoView } 对象,而不是 DOM 节点。这样你就可以把暴露给外界的 DOM 节点信息限制到最少。
import { useRef } from 'react'; import MyInput from './MyInput.js'; export default function Form() { const ref = useRef(null); function handleClick() { ref.current.focus(); // 这将不起作用,因为 DOM 节点没有被暴露: // ref.current.style.opacity = 0.5; } return ( <form> <MyInput placeholder="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>
);
});