当我试图 extends 一个被 connect 装饰过的 React Component 时

问题

起因是这样的,一开始在项目中写了一个原始的 Popup 组件(通过 react-redux 的 connect 方法接入 redux),后来有了一个更高级的需求,我就想要直接 extends 这个原始的 Popup 组件,然后覆写一些方法。 然后遇到了一个问题,继承之后的子组件没有父组件的自定义方法。

解疑

最初一度怀疑是否是 babel 的 decorator 插件有问题(毕竟不是浏览器天然支持的特性),但是仔细看了 decorator 插件的源码,发现并不存在这种问题。 后来在这个插件的 issues 里看到了这么一个 issue,问题并不算是同一个,但是作者提到,react-redux 的 connect 方法可能实现有一些问题(当然这个地方并不是),我就想到去看看 connect 的实现。 然后就看到了底层方法 connectAdvanced 中有这么一句代码:

export default connectAdvanced() {
    // ...

    return hoistStatics(Connect, WrappedComponent);
}

hoistStatics 是一个第三方库 hoist-non-react-statics,它干的事情也比较简单:

Copies non-react specific statics from a child component to a parent component. Similar to Object.assign, but with React static keywords blacklisted from being overridden.

简单来说,就是复制所有非 React 自有的属性,因为使用了 Object.getOwnPropertyNames()的方法,所以我们定义在 Component 上的所有方法就不会被复制到这个新的对象上了。 而在 connectAdvanced 内部,render 时实际是这样的:

render() {
  const selector = this.selector
  selector.shouldComponentUpdate = false

  if (selector.error) {
      throw selector.error
  } else {
      return createElement(WrappedComponent, this.addExtraProps(selector.props))
  }
}

通过 createElement 方法给原始的 WrappedComponent 传入额外的 props,来实现 connect 的效果。 总之,最终 connect 方法返回这个对象,其实和原始的 Component 的结构并不一样,虽然它的 WrappedComponent 属性指向了原始的 Component,但是显然在我们的场景下,并不能使用。

那怎么办呢

通常的推荐解决方式就是『高阶组件』,这样就屏蔽了 connect 的逻辑。

为什么要用 hoist-non-react-statics 这种模式

关于这个问题,作者在 这一个 issue 的回答中 也说了很多,总结来说,就是为了避免使用者可以直接的接触到 connect 之后的组件的内部方法,防止因为各种改动导致组件的 "break",这也算是为了提升健壮性。