React 之 高阶组件的理解

1.基本概念

高阶组件是参数为组件,返回值为新组件的函数。

2.举例说明

① 装饰工厂模式

组件是 react 中的基本单元,组件中通常有一些逻辑(非渲染)需要复用处理。这里我们可以用高阶组件对组件内部中的一些通用进行封装。

未封装时,相同的逻辑无法复用:

渲染评论列表

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // 假设 "DataSource" 是个全局范围内的数据源变量
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 订阅更改
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除订阅
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 当数据源更新时,更新组件状态
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

渲染博客列表

lass BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

借用高阶组件,封装公用逻辑:

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

组件加工(加工:处理公用逻辑)工厂,接受旧组件,返回新组件:

// 此函数接收一个组件...
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} />;
    }
  };
}

 

② 高阶组件柯里化

高阶组件是一个参数是组件返回值也是组件的函数,那么我们借助函数柯里化,确保最终返回的函数是高阶组件就可以了。

import React, { Component } from "react";

const isEmpty = prop =>
  (prop && prop.hasOwnProperty("length") && prop.length === 0) ||
  (prop.constructor === Object && Object.keys(prop).length === 0);

export default loadingProp => WrappedComponent => {
  const hocComponent = class extends Component {
    componentDidMount() {
      this.startTimer = Date.now();
    }

    componentWillUpdate(nextProps, nextState) {
      console.log(nextProps)
      if (!isEmpty(nextProps[loadingProp])) {
        this.endTimer = Date.now();
      }
    }

    render() {
      const myProps = {
        loadingTime: ((this.endTimer - this.startTimer) / 1000).toFixed(2)
      };

      return isEmpty(this.props[loadingProp]) ? (
        <div>loading...</div>
      ) : (
        <WrappedComponent {...this.props} {...myProps} />
      );
    }
  };

  return hocComponent;
};

其中 ,而 loadingProp => ... 是一个返回值为高阶组件的函数,其返回结果 WrappedComponent => {...} 是一个单参数(即被包裹的组件)的高阶组件。这样做有什么好处呢?为什么不写一个高阶组件并将参数与包装组件一并传递过去?高阶组件受限于返回值必须是组件,因此它无法柯里化。而在高阶组件之上再构建一个函数就能进行柯里化。同时,返回的结果是单参数(被包裹组件)的高阶组件可以直接做 hoc 嵌套,即 一个 hoc 嵌套 另一个 hoc(因为传入值、传出值都是 组件)。此外的对于 hoc 嵌套调用,可以借助 compose 工具函数 进行扁平化处理。

许多第三方库提供都提供了 compose 工具函数,包括 lodash (比如 lodash.flowRight), ReduxRamda

③ 设定 hoc 的显示名称 displayName

为了方便调试,设定 hoc 的显示名称类似: WithSubscription(CommentList)

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

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

3. 注意事项

① 不要在 render 方法中使用 hoc

因为 hoc 会产生新的组件,而 redner 方法经常被调用,所以会不断产生新的组件(而在 react 的 diff 算法中会将之前旧的卸载而替换成新的,这不仅仅会对性能造成影响,同时重新挂载组件会导致该组件及其所有子组件的状态丢失。

②务必复制静态方法

手动一个个复制

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;
}

  

③ Ref 需要转发

虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。

使用 hoc 包裹组件

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

  // ...
}

// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton);

形式上导入的是原组件,实际上导入的是 hoc 包裹的原组件。这时如果直接传 ref 到该组件,实际上 ref 并没有传递到原组件中,而停留在 hoc 组件上。

import FancyButton from './FancyButton';

const ref = React.createRef();

// 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
// 这意味着我们不能调用例如 ref.current.focus() 这样的方法
<FancyButton
  label="Click Me"
  handleClick={handleClick}
  ref={ref}
/>;

这里我们需要对停留在 hoc 组件中的 ref 进行转发,使其传递到原组件中

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

233