[译]React高级指引5:Refs转发

1,565 阅读5分钟

原文链接:reactjs.org/docs/forwar…

引言

Ref转发是一项自动从组件中将ref传递给其子组件的技术。但这在绝大部分组件中不是必需的。但是这对某些组件来说会非常有用,尤其是在某些可复用的第三方组件库中。常见的场景我们将在下面的内容描述。

将refs转发给DOM组件

我们来考虑下面的例子,FancyButton组件渲染了一个原生button DOM元素:

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

React组件隐藏了它们的实现细节,包括它们的渲染输出。使用FancyButton的其他组件通常不需要获取它内部的button DOM元素的ref。这是非常好的,因为它避免了组件过于依赖其他组件的DOM结构。

尽管这样封装代码对于应用层级的组件(比如FeedStoryComment)来说是理想的,但这对类似FancyButtonMyTextInput这类的高复用性的“叶”组件来说会非常不方便。这些组件会在应用中充当类似原生DOM buttoninput来使用,因此对于管理焦点,选择或动画效果来说获取它们的DOM节点是不可避免的操作。

Refs转发是一个选择性加入的特性,它让组件接收它们收到的ref并将它传递(或称为转发)到更深层级的子元素中。

在下面的例子中,FancyButton使用React.forwardRef获取传递给它的ref,并转发给它渲染的button元素。

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

// 你现在可以直接获取DOM元素的引用了:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

用这种方法,使用了FancyButton组件的组件就得到了FancyButton组件中的button元素的引用并且在必要时可以使用它——就像直接操纵DOM元素一样。

下面是对上面的例子代码运行的详细解释:

  1. 我们通多调用React.createRef来创建一个React ref并将它赋值给ref变量。
  2. 通过声明JSX属性<FancyButton ref={ref}>ref传递给FancyButton
  3. React将ref作为第二个参数传递进forwardRef内的(props, ref) => ...函数。
  4. 我们通过声明JSX属性<button ref={ref}>ref转发给button元素。
  5. 当ref被绑定时,我们就可以通过ref.current来获取<button>DOM节点。

注意: 第二个参数ref只存在于当你调用React.forwardRef来定义组件时。正常的函数组件和class组件不会接收ref作为参数,也无法在props中获取到ref。 . ref转发并不局限于在DOM元素上使用,你也可以将ref转发给一个class组件实例。

组件库维护者的注意事项

当你在组件库中使用forwardRef时,你应该把它当作一个破环性的更改并且发布一个新版本。因为你的组件库会有和以前的版本显著的不同(比如refs被分配给了谁,导出了声明类型),这可能会破坏使用者的应用和那些依赖老版本的组件。

处于同样的原因,当React,forwardRef存在时有条件地调用它是我们不推荐的:它改变了你的库的行为,并在用户更新React时会破坏用户的应用。

在高阶组件中转发refs

转发refs对高阶组件(也被称为HOC)十分有用。在下面的例子中我们使用高阶组件在控制台打印它接收的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传递给它包裹的组件,所以渲染的结果将会是相同的。在下面的例子中,我们通过这个HOC将所有传递给“fancy button”组件的的props都打印出来:

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

  // ...
}
//我们通过export LogProps来代替export FancyButton
//这依然会渲染FancyButton
export default logProps(FancyButton);

但在上面的例子中有一点需要注意:refs不会透传下去,因为ref不是一个porp。就像key一样,React处理它的方式是不同的。如果你为HOC添加了一个ref,那么这个ref将会成为最外面的容器组件的引用,而不是被包裹组件的。

这意味着原本打算给FancyButton使用的ref实际上被绑定在了LogProps组件上:

import FancyButton from './FancyButton';

const ref = React.createRef();

//我们引入的FancyButton组件实际上是LogProps高阶组件
//即使最终的渲染结果是相同的,
//我们的ref指向的是LogProps而不是内部的FancyButton组件!
//这意味着我们不能调用ref.current.focus()
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

但幸运的是,我们可以通过显式地调用React.forwardRef API将refs转发给内部的FancyButton组件。React.forwardRef接收一个render函数(接收了propsref作为参数)并返回一个React节点:

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;
      
      //将自定义的“forwardRef”最为ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  //注意React.forwardRef提供的第二个参数“ref”
  //我们可以把它作为一个常规prop传递给LogProps,
  //比如forwardRef。
  //这个forwardRef prop之后将会被Component绑定
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

在DevTools中显式自定义名称

React.forwardRef接收一个render函数。React DevTools使用这个函数来决定为ref转发组件显示的内容。

比如下面的例子,组件将会在DevTools中显示“ForwardRef“:

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

如果你为render函数起了名字,那么DevTools中显示的内容将会包含这个名字(比如:”ForwardRef(myFunction)”):

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

你甚至可以通过设置函数的displayName属性来使展示内容包括你包裹的组件:

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

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

  //给这个组件一个展示名称
  //使其在DevTools中对用户更有帮助
  //比如 "ForwardRef(logProps(MyComponent))"
  const name = Component.displayName || Component.name;
  forwardRef.displayName = `logProps(${name})`;

  return React.forwardRef(forwardRef);
}