高阶组件

95 阅读4分钟

高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身不是React API的一部分,它是一种基于React的组合特性而形成的设计模式HOC是参数为组件,返回值为新组件的函数。Component=>Component2; 例如Redux的connect,Relay的createFragmentContainer。

使用HOC解决横切关注点问题

以前会推荐使用mixins解决横切关注点的问题:可以在不同组件间使用相同的函数,但有如下的问题:

mixins引入了隐式依赖

  • 组件的render方法可能会引入一些未在类中定义的方法,删除是否安全?
  • mixins不形成层次结构:它们被扁平化并在同一个命名空间中运行

mixins导致名称冲突

  • 当有两个mixins同时定义了一个同名函数A,就不能同时引入这两个mixins并使用相同函数名A的函数,自己组件也不能定义名为A的函数。

mixins导致复杂性像滚雪球一样

HOC是纯函数,没有副作用

可以不停地包裹组件,生命周期的方法也不会被覆盖,每个组件的生命周期定义的方法都会被执行。HOC不需要关心数据的使用方法或原因,而被包装组件也不需要关心数据是怎么来的。HOC只要确保有一个入参是组件,其他入参的个数和类型都不需要限制。

HOC里不要改变组件原型,不要修改入参的组件

约定:将不相关的 props 传递给被包裹的组件

HOC为组件添加特性,本身不应该大幅改变约定。HOC返回的组件与原组件应保持类似的接口。HOC应该透传与自身无关的props。

约定:最大化可组合性

建议编写组合工具函数
如:compose(f, g, h) 等同于 (...args) => f(g(h(...args)))

约定:包装显示名称以便轻松调试

将HOC组件的displayName设置成HOC名(被包装组件名)

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
    // ...并返回另一个组件...
    return class extends React.Component {
      constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.state = {
          data: selectData(DataSource, props)
        };
      }
  
      componentDidMount() {
        // ...负责订阅相关的操作...
        DataSource.addChangeListener(this.handleChange);
      }
  
      componentWillUnmount() {
        DataSource.removeChangeListener(this.handleChange);
      }
  
      handleChange() {
        this.setState({
          data: selectData(DataSource, this.props)
        });
      }
  
      render() {
        // ... 并使用新数据渲染被包装的组件!
        // 请注意,我们可能还会传递其他属性
        return <WrappedComponent data={this.state.data} {...this.props} />;
      }
    };
  }

注意事项

不要再render方法中使用HOC:

react的diff算法使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。如果从render返回的组件与前一个渲染中的组件相同(===),则react通过将子树与新子树进行区分来递归更新子树。如果它们不相等,则完全卸载前一个子树。

render() {
  // 每次调用 render 函数都会创建一个新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
  return <EnhancedComponent />;
}

重新挂载组件会导致该组件及其所有子组件的状态丢失。 这应该在组件之外创建HOC。也可以将render的重复内容提取成组件。

务必复制静态方法

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须准确知道应该拷贝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

但要这样做,你需要知道哪些方法应该被拷贝。你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

除了导出组件,另一个可行的方案是再额外导出这个静态方法。

// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';

Refs不会被传递

refs不会像props那样被传递,因为这是react专门处理的。如果将ref添加到HOC的返回组件中,则ref引用指向容器组件,而不是被包装组件,解决方案是:React.forwardRef。

参考: 高阶组件