[译]React高级话题之Forwarding Refs

前言

本文为意译,翻译过程中掺杂本人的理解,如有误导,请放弃继续阅读。

原文地址:Forwarding Refs

Ref forwarding是一种将ref钩子自动传递给组件的子孙组件的技术。对于应用的大部分组件,这种技术并不是那么的必要。然而,它对于个别的组件还是特别地有用的,尤其是那些可复用组件的类库。下面的文档讲述的是这种技术的最常见的应用场景。

正文

传递refs到DOM components

假设我们有一个叫FancyButton的组件,它负责在界面上渲染出一个原生的DOM元素-button:

function FancyButton(props) {
  return (
    <button className="FancyButton">
      {props.children}
    </button>
  );
}
复制代码

一般意义来说,React组件就是要隐藏它们的实现细节,包括自己的UI输出。而其他引用了<FancyButton>的组件也不太可能想要获取ref,然后去访问<FancyButton>内部的原生DOM元素button。在组件间相互引用的过程中,尽量地不要去依赖对方的DOM结构,这属于一种理想的使用场景。

对于一些应用层级下的组件,比如<FeedStory><Comment>组件(原文档中,没有给出这两个组件的实现代码,我们只能顾名思义了),这种封装性是我们乐见其成的。但是,这种封装性对于达成某些“叶子”(级别的)组件(比如,<FancyButton><MyTextInput>)的高可复用性是十分的不方便的。因为在项目的大部分场景下,我们往往是打算把这些“叶子”组件都当作真正的DOM节点button和input来使用的。这些场景可能是管理元素的聚焦,文本选择或者动画相关的操作。对于这些场景,访问组件的真正DOM元素是在所难免的了。

Ref forwarding是组件一个可选的特征。一个组件一旦有了这个特征,它就能接受上层组件传递下来的ref,然后顺势将它传递给自己的子组件。

在下面的例子当中,<FancyButton>通过React.forwardRef的赋能,它可以接收上层组件传递下来的ref,并将它传递给自己的子组件-一个原生的DOM元素button:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 假如你没有通过 React.createRef的赋能,在function component上你是不可以直接挂载ref属性的。
// 而现在你可以这么做了,并能访问到原生的DOM元素:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
复制代码

通过这种方式,使用了<FancyButton>的组件就能通过挂载ref到<FancyButton>组件的身上来访问到对应的底层的原生DOM元素了-就像直接访问这个DOM元素一样。

下面我们逐步逐步地来解释一下上面所说的是如何发生的:

  1. 我们通过调用React.createRef来生成了一个React ref,并且把它赋值给了ref变量。
  2. 我们通过手动赋值给<FancyButton>的ref属性进一步将这个React ref传递下去。
  3. 接着,React又将ref传递给React.forwardRef()调用时传递进来的函数(props, ref) => ...。届时,ref将作为这个函数的第二个参数。
  4. (props, ref) => ...组件的内部,我们又将这个ref 传递给了作为UI输出一部分的<button ref={ref}>组件。
  5. <button ref={ref}>组件被真正地挂载到页面的时候,,我们就可以在使用ref.current来访问真正的DOM元素button了。

注意,上面提到的第二个参数ref只有在你通过调用React.forwardRef()来定义组件的情况下才会存在。普通的function component和 class component是不会收到这个ref参数的。同时,ref也不是props的一个属性。

Ref forwarding技术不单单用于将ref传递到DOM component。它也适用于将ref传递到class component,以此你可以获取这个class component的实例引用。

组件类库维护者的注意事项

当你在你的组件类库中引入了forwardRef,那么你就应该把这个引入看作一个breaking change,并给你的类库发布个major版本。这么说,是因为一旦你引入了这个特性,那你的类库将会表现得跟以往是不同( 例如:what refs get assigned to, and what types are exported),这将会打破其他依赖于老版ref功能的类库和整个应用的正常功能。

我们得有条件地使用React.forwardRef,即使有这样的条件,我们也推荐你能不用就不要用。理由是:React.forwardRef会改变你类库的行为,并且会在用户升级React版本的时候打破用户应用的正常功能。

高阶组件里的Forwarding refs

这种技术对于高阶组件来说也是特别有用的。假设,我们要实现一个打印props的高阶组件,以往我们是这么写的:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
}
复制代码

高阶组件logProps将所有的props都照样传递给了WrappedComponent,所以高阶组件的UI输出和WrappedComponent的UI输出将会一样的。举个例子,我们将会使用这个高阶组件来把我们传递给<FancyButton>的props答应出来。

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Rather than exporting FancyButton, we export LogProps.
// It will render a FancyButton though.
export default logProps(FancyButton);
复制代码

上面的例子有一个要注意的地方是:refs实际上并没有被传递下去(到WrappedComponent组件中)。这是因为ref并不是真正的prop。正如key一样,它们都不是真正的prop,而是被用于React的内部实现。像上面的例子那样给一个高阶组件直接传递ref,那么这个ref指向的将会是(高阶组件所返回)的containercomponent实例而不是wrapper component实例:

import FancyButton from './FancyButton';

const ref = React.createRef();

// The FancyButton component we imported is the LogProps HOC.
// Even though the rendered output will be the same,
// Our ref will point to LogProps instead of the inner FancyButton component!
// This means we can't call e.g. ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;
复制代码

幸运的是,我们可以通过调用React.forwardRef这个API来显式地传递ref到FancyButton组件的内部。React.forwardRef接收一个render function,这个render function将会得到两个实参:props和ref。举例如下:

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
    + const {forwardedRef, ...rest} = this.props;

      // Assign the custom prop "forwardedRef" as a ref
    + return <Component ref={forwardedRef} {...rest} />;
    - return <Component {...this.props} />;
    }
  }

  // Note the second param "ref" provided by React.forwardRef.
  // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
  // And it can then be attached to the Component.
  + return React.forwardRef((props, ref) => {
  +  return <LogProps {...props} forwardedRef={ref}   />;
  + });
}
复制代码

在DevTools里面显示一个自定义的名字

React.forwardRef接收一个render function。React DevTools将会使用这个function来决定将ref forwarding component名显示成什么样子。

举个例子,下面的WrappedComponent就是ref forwarding component。它在React DevTools将会显示成“ForwardRef”:

const WrappedComponent = React.forwardRef((props, ref) => {
  return <LogProps {...props} forwardedRef={ref} />;
});
复制代码

假如你给render function命名了,那么React DevTools将会把这个名字包含在ref forwarding component名中(如下,显示为“ForwardRef(myFunction)”):

const WrappedComponent = React.forwardRef(
  function myFunction(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }
);
复制代码

你甚至可以把wrappedComponent的名字也囊括进来,让它成为render function的displayName的一部分(如下,显示为“ForwardRef(logProps(${wrappedComponent.name}))”):

function logProps(Component) {
  class LogProps extends React.Component {
    // ...
  }

  function forwardRef(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }

  // Give this component a more helpful display name in DevTools.
  // e.g. "ForwardRef(logProps(MyComponent))"
  const name = Component.displayName || Component.name;
  forwardRef.displayName = `logProps(${name})`;

  return React.forwardRef(forwardRef);
}
复制代码

这样一来,你就可以看到一条清晰的refs传递路径:React.forwardRef -> logProps -> wrappedComponent。如果这个wrappeedComponent是我们上面用React.forwardRef包裹的FancyButton,这条路径可以更长:React.forwardRef -> logProps -> React.forwardRef -> FancyButton -> button。

猜你喜欢

转载自juejin.im/post/5c0dd44b51882530e4617e92