将Ref作为属性传递给子组件
在真实的开发场景中,我们并不需要一个包含了大量信息的巨形组件。更有可能的是,我们需要把input框抽象成一个组件:如此一来,我们可以封装其逻辑与样式,并多次复用它。甚至,我们还可以为这个组件的右边添加一些图标。
const InputField = ({ onChange, label }) => {
return (
<>
{label}
<input
type="text"
onChange={(e) => onChane(e.targe.value)}
/>
</>
)
}
但是错误处理以及提交功能仍然会放在Form中,而不是(单个的)input里
const Form = () => {
const [name, setName] = useState('');
const onSubmitClick = () => {
if (!name) {
// deal with empty name
} else {
// submit the data here!
}
};
return (
<>
<InputField label="name" onChange={setName} />
<button onClick={onSubmitClick}>
Submit the form!
</button>
</>
);
};
我该如何让input组件得到Form组件的指令,进行聚焦呢?常用的方法是传递一个属性进去,并调用相关的回调。可以传递一个focusItself属性,并监听它。当focusItself为true时,调用相关的回调函数。但是,这个回调函数可能只调用一次。
// don't do this! just to demonstate how it could work in theory
const InputField = ({ onChange, focusItself }) => {
const inputRef = useRef(null);
useEffect(() => {
if (focusItself) {
// focus input if the focusItself prop changes
// will work only once, when false changes to true
inputRef.current.focus();
}
}, [focusItself]);
// the rest is the same here
};
我可以为input组件添加一个onBlur回调:当input组件失焦时,把focusItself设置为false。
幸运的是,还有另一种方法。我们通过使用Ref,并传递给InputField。
const Form = () => {
// create the Ref in Form component
const inputRef = useRef(null);
...
}
而InputField组件有一个属性来接受Ref,并渲染一个有了Ref的input框。这样一来,就不用在InputField内创建Ref了
const InputField = ({ inputRef }) => {
// the rest of the code is the same
// pass ref from prop to the internal input component
return <input ref={inputRef}... />
}
Ref是一个可变对象,其本身就是按这样的方式设计的。当我们将它传递给一个元素时,React 会在底层对其进行修改。而且将要被修改的这个对象是在Form组件中声明的。所以一旦InputField组件被渲染,Ref对象就会发生变化,并且我们的Form组件将能够通过inputRef.current访问到input的 DOM 元素。
const Form = () => {
// create the Ref in Form component
const inputRef = useRef(null);
useEffect(() => {
// the "input" element, that is rendered inside InputField, will be here
console.log(inputRef.current);
}, []);
return (
<>
{/* Pass Ref as prop to the input field component */}
<InputField inputRef={inputRef} />
</>
);
};
或者在我们的提交回调函数中,我们可以调用 inputRef.current.focus(),这和之前的代码完全一样。
代码示例: advanced-react.com/examples/09…
使用forwardRef将Ref作为属性传递给子组件
也许你在疑问,为什么我们给Ref属性的命名为inputRef,而不是ref:这不是表面上那么简单的。ref不是一个真实的属性;它是一个被保留的关键字。在以前,我们还在写类组件的时候,如果我们传递一个Ref给类组件,给类组件的实例的.current的值,就是这个Ref的值。
但是,函数组件并没有类实例。所以,我们会在控制台得到这样一个提示:"Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"
const Form = () => {
const inputRef = useRef(null);
// if we just do this, we will get a warning in console
return <InputField ref={inputRef}>
}
为了让上述操作能够生效,我们需要向 React 表明这个引用(ref)是有意为之的,而且我们想要用它来做一些事情。我们可以借助 forwardRef 函数来实现这一点:它接收我们的组件,并将来自 ref 属性的引用作为组件函数的第二个参数注入进来,就在 props 之后。
// normally, we'd have only props there
// but we wrapped the component's function with forwardRef
// which injects the second argument - ref
// if it's passed to this component by its consumer
const InputField = forwardRef((props, ref) => {
// the rest of the code is the same
return <input ref={ref} />;
});
我们可以把上面的代码切分为两个变量,以提升代码的可读性:
const InputFieldWithRef = (props, ref) => {
// the rest is the same
}
// this one will be used by the form
export const InputField = forwardref(InputFieldWithRef);
现在,Form组件可以传递Ref给InputField 组件了:
return <InputField ref={inputRef}>
至于使用forwardRef或者使用简单的Ref作为属性,本质上还是个人的编码偏好的问题:其结果都是一样的。