React 学习笔记(6)—— 高阶组件(HOC)

174 阅读3分钟

高阶组件(HOC)是 React 一种复用组件逻辑的技巧。HOC 是一种基于 React 组合特性而形成的设计模式,而非 API。

高阶组件是一个函数,参数是组件,返回值是一个新组件。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

组件是把 props 转化为 UI,高阶组件是把组件转换为组件。

基本范式

function withSubscription(WrappedComponent, selectData) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        data: selectData(DataSource, props),
      };
      this.handleChange = this.handleChange.bind(this);
    }
    
    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} />;
    }
  };
}

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource, props) => {
    return DataSource.getCommentList(props.id);
  }
);
  1. HOC 使用组合而非继承的方式来复制参数组件的行为
  2. HOC 通过将组件包装在容器组件中来组成新组件,而不是修改参数组件
  3. HOC 是纯函数,没有副作用
  4. HOC 不需要关心数据的使用方式,而包装组件也不需要关心数据的来源;容器组件和包装组件之间的契约完全由 props 来确定

容器组件将高级和低级关注点分离,由容器组件管理订阅和状态,由包装组件处理 UI。HOC 可视为参数化的容器组件。

注意:不要在 HOC 中修改原始组件,修改原始组件会产生副作用,从而与外界代码耦合;而且以修改组件的方式来扩展功能只适用于 class 组件,不适用于函数组件。使用组合可实现同样的功能,且适用于所有类型的组件;作为纯函数的 HOC 还可以和其它 HOC(甚至自身)结合起来使用。

约定

1. 将与自身无关的 props 传给包装组件

render() {
  const { extraProps, ...passThroughProps } = this.props;
  const injectedProps = someStateOrInstanceMethod;
  return (
    <WrappedComponent injectedProps={injectedProps} {...passThroughProps} />
  );
}

HOC 的目的是为包装组件增加特性,因此不应过度改变原始组件的使用方式(接口)。

2. 最大化可组合性

HOC 可以仅接收包装组件作为唯一参数,也可以接收多个参数,比如接收配置对象作为第二个参数。

同一类 HOC 可使用一个高阶函数来表示,依参数返回不同的 HOC,比如下面的 connect

单参数 HOC 具有类型签名 Component => Component,而输入类型和输出类型相同的函数容易组合起来嵌套调用。

const enhance = compose(withRouter, connect(commentSelector));
const EnhancedComponent = enhance(WrappedComponent);

compose 是一个组合工具函数,返回一个由所有传入的函数复合而成的函数。

3. 包装显示名称以方便调试

HOC 和普通组件一样,会显示在 React DevTools 中,为方便调试,可在 HOC 中设置容器组件的显示名称(displayName)。

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

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || "Component";
}

注意事项

1. 不要在 render 方法中使用 HOC

render 方法中使用 HOC 会导致性能问题,而且还会导致该组件及其所有子组件的状态丢失。在组件外创建的 HOC 只会创建一次,可以解决这一问题。

极少数情况需要动态调用 HOC,此时可以在组件生命周期方法或构造函数中调用。

2. 务必复制静态方法

包装组件的静态方法需要复制到容器组件上,使用 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;
}

也可以直接将静态方法从模块中导出。

3. Refs 不会被传递

使用 Refs 转发(如 React.forwardRef)可以解决 ref 属性不会被 HOC 传递的问题。