一、描述

forwardRef() 这个 API 在 react v16.4 就已经存在了,而在 v16.6 的文档中,突然提到了前面,应该是想让开发者更加注意吧。

同时 React.forwardRef 也增加了一些新的内容和用法,值得看看。

本质上, Ref forwarding 就是将 ref 通过组件自动传递给子节点,能够提高组件的复用性,不至于自己写一些组件,然后因为 ref 的问题,在复用的时候出现问题。

一般来说,组件是不需要 forwading ref 这个东西,简单的直接将 ref 挂载 DOM 或者挂载组件上就可以了,文档中列举了一些可以使用 forwarding ref 的情景。

二、将 forwarding refs 挂到原生DOM 组件上

1、forwardRef 和 createRef

下列内容代码部分和文档基本上没太大差别

比如我们写了一个组件是 FancyButton,是一个函数声明组件,并且 button 标签上挂了一个 ref,基本代码如下:

const FancyButton = (props) => {
    return (
        <button className='FancyButton' ref='btn'>
          {props.children}  
        </button>
    )
}

通常来说,如果我们要在 FancyButton 的父组件中,去调用 <button> 的 ref,都是给 FancyButton 组件一个 ref,比如是 fancyBtn,然后通过 this.refs.fancyBtn.refs.btn 去间接地调用。

但是,请注意,FancyButton 是一个 函数式组件,而 Function components cannot have refs.

因此我们的这种非法想法就无疾而终了,当然也可以通过声明 class 组件的方式去完成类似链式调用的东西( 同时请注意。class 组件也不能传递 ref 给子组件,只能通过上面这种类似链式调用的方式去间接调用)。

如果非要使用函数声明组件去实现,可以借助 Forwarding Ref,实际上 Forwarding Ref 是允许组件通过接受 ref,然后将这个 ref 进一步的往下传递给其子节点,因此用 Forwarding 来阐述这种转发或者传递的思想。

比如改进上面的 FancyButton

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

比较大的区别是,在创建 FancyButton 的时候,没有直接使用一个函数,而是通过 React.forwardRef() 方法(有点高阶组件的意思)的形式对之前的函数进行一次包装,此时多了一个 ref,并且将 ref 挂到了 button 上面。

请注意,默认的 React 组件,ref 如同 key 一样,是不会往子组件中传递的,这个是由 React 框架层面处理的

如果想要使用现在 FancyButton 并且把一个 ref 传递给子组件,则需要下面的形式:

const ref = React.createRef();
<FancyButton ref={ref}>ClickMe</FancyButton>

文档中有一个描述上述事件发生的过程:

  1. 通过 React.createRef 创建了一个 React Ref,并且赋值给 ref 变量
  2. 通过 <FancyButton ref={ref}> 将 ref 变量传递给了 FancyButton 组件的 ref 属性
  3. React 通过 (props, ref)=>{} 这个方法,将 上面的ref 传递给了第二个参数 ref
  4. 在 FancyButton 中奖 ref 传递给了 <button> DOM 组件的 ref
  5. 上面完成后, ref.current 会挂载到 <button> DOM 组件上

ref.current 的基本内容如下(其实就是 button 组件)

1.png

如果输出 console.log(findDOMNode(ref.current)) 则会直接输出 DOMNode:

<button class="FancyButton">ClickMe</button>

2、注意事项

需要注意的是,通过 React.forwardRef 声明的组件的第二个参数 ref 只有使用 React.forwardRef 的时候才会有,如果是正常的使用函数声明组件,是不会存在 ref 参数的,就像上面提到的, 这个是在 React 框架层面完成的事情。

二、 组件维护者的特别提醒

其实这个提醒很人性, React 的建议是如果你使用了 Forwarding Ref ,那么建议你维护的组件需要发一次主要版本,并且将其视为重大更新,因为 Forwarding Ref 是明显不同于之前使用 ref 的行为和表现,怎么传递,会返回什么等等,如果贸然使用并且更新到组件上,很可能导致旧组件的一些依赖直接挂掉。

因此在使用 Forwarding Ref 的时候,应当格外的小心,很大程度上,改变了以往使用组件的方式或者库的表现形式,就像是我肯定不会去使用一样。

三、传递给高阶组件(HOCs)

高阶组件反正一直是个很神奇的东西,用的好的人称赞的很,也有人觉得没有存在的必要,这个完全看怎么取舍。

按照官方文档给的示例,通过高阶组件包装组件,使其在 mounted 的时候打印前后的 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;
}

而现在我们将 FancyButton 改为一个 class 组件:


class FancyButton extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <button className='FancyButton'>
        {this.props.children}  
      </button>
    )
  }
}
// 能够打印日志的 FancyButton
const LogFancyButton = logProps(FancyButton);

首先为了使用 高阶组件,声明了变量 LogFancyButton 来表示新的 FancyButton 组件,能够打印日志(没有拆分文件的形式导入和导出)。

在 App 中使用的时候如下,(加了个点击事件,用来每次更改 state,更新 this.props.children,能够响应高阶组件的 componentDidUpdate

const LogFancyButton = logProps(FancyButton);
const ref = React.createRef();
class App extends Component {
  state = {
    name: 'clickme'
  }
  componentDidMount(){
    console.log(findDOMNode(ref.current))
  }
  clickHandle = () => {
    this.setState({
      name: 'clickme' + Date.now()
    });
  }
render() {
  return ( 
      <div className="App">
       <LogFancyButton ref={ref} onClick={this.clickHandle}>{this.state.name}</LogFancyButton>
      </div>
  );
}
}

export default App;

效果如下:

GIF.gif

但是,如果输出 ref,会发现, ref.current 并不是期望的 button,而是挂到了 logProps 上阿敏

2.png

原因是一样的,ref 是不能像 props 一样,往下面传递的,因此想要往下面传递,必须要用到 React.forwardRef 这个 API。

改造高阶组件方法如下:

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;
      return <Component ref={forwardedRef} {...rest} />;
    }
  }
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

现在的高阶组件方法和之前的有很大的不同,首先,组件方法中没有直接 将 LogProps 这个 class return 回来,而是像之前我们使用 React.forwardRef 一样,通过 return React.forwardRef((props, ref) => {}) 的方式,去将 LogProps 重新包装了一下。

不过在传递 ref 的时候,并没有直接使用 ref={ref},而是换了个名称 forwardedRef={ref}

同时,在 LogProps 的 render 方法中,除了将原有的 this.props 往下传递(通过 ...rest)之外,将 forwardedRef 单独拿出来并再次改名成为 ref={forwardedRef} 传递给子组件,这样子才能做到将 ref 跳过 LogProps 传递给 Component(logPros 高阶组件的参数组件)

四、在 devtool 中显示一个自定义名称

React.forwardRef 的函数参数可以是具名方法,比如 React.forwardRef( function funcName(props, ref){}) 这样子的形式,指定了方法名之后,在 DevTool 中能够根据这个方法名去显示。

比如上面的方法会在 React Dev Tool 中如下显示:

333.png

当然,也可以动态的更改 funcName(props, ref) 的 displayName,只要在将 funcName 方法放进 React.forward 之前就行。

比如下面的方式:

  function funcName(props, ref) {
    return <LogProps {...props} forwardedRef={ref} />;
  }
  const name = Component.displayName || Component.name;
  funcName.displayName = `logProps-${name}`
  return React.forwardRef(funcName);

在 DevTool 中会显示如下:

444.png