第九章 Refs:从存储数据到指令式API 下

229 阅读3分钟

文章出处:www.advanced-react.com/

专栏地址:juejin.cn/column/7443…

将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作为属性,本质上还是个人的编码偏好的问题:其结果都是一样的。

代码示例: advanced-react.com/examples/09…