关于React高阶组件

高阶组件的定义

高阶组件(Higher-Order Component)是一个函数,以下简称 HOC,接受一个组件,返回一个新组件。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

HOC 不应该修改传入组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from "react";

export default function higherOrderComponent(WrappedComponent) {
return class HOCComponent extends React.Component {
componentDidUpdate(prevProps) {
console.log("Current props: ", this.props);
console.log("Previous props: ", prevProps);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}

高阶组件不是只能接收一个参数,它也可以接收多个参数。项目中通常使用的 React Redux 的 connect 函数,它的 HOC 签名是这样的:
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

这样的用法实现方式大概类似以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react'

function HOCFunction = (param) => (WrappedComponent) => {
return class HOCComponent extends Component {
componentWillMount() {
console.log('通用逻辑', param)
}

render() {
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}

class MyComponent1 extends Component {
render() {
return <div>{this.props.data}</div>
}

//省略其他逻辑...
}

const resultComponent = HOCFunction('param1 test')(MyComponent1);

高阶组件(HOC) 使用场景

  • 代码复用:这是高阶组件最基本的功能。组件是 React 中最小单元,两个相似度很高的组件通过将组件重复部分抽取出来,再通过高阶组件扩展,增删改 props,可达到组件可复用的目的;
  • 条件渲染:控制组件的渲染逻辑,常见场景:鉴权、新手指导;
  • 生命周期捕获/劫持:借助父组件子组件生命周期规则捕获子组件的生命周期,常见场景:打点。

使用高阶组件的注意事项:

  • Refs 不会被传递给被包装组件

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

1
2
3
4
5
const enhanceComponent = HOCComponent(WrappedComponent)

<enhanceComponent ref={el => this.$el = el} />

// 此时,ref指向HOCComponent,而不是WrappedComponent

在应用中如果需要和 dom 交互后者父组件想要调用子组件的某些方法,我们就需要获取 ref。react-redux 中的 connect 在开发中是常用到的高阶组件,如果我们想通过 ref 去拿被 connect 包裹的组件是无法获取到结果的,通常会手动指定一下调用 connect 方法的最后一个参数,指定 withRef 为 true, 这样就能拿到被包裹组件的 ref 了。
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], {withRef: true})(CustomComponent)

如果是自定义的 HOC,又希望拿到被包裹组件的 ref,怎么做呢?答案是 React.forwardRef API(React 16.3 中引入), 通过 React.forwardRef 在高阶组件中转发 refs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}

// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
  • 不会复制静态方法

有时我们在原始组件定义了静态方法,当用 HOC 把原始组件包装后,原始组件的静态方法在 HOC 之后的新组件里是不存在的

1
2
3
4
5
6
7
8
9
// 定义静态函数
WrappedComponent.staticMethod = function () {
/*...*/
};
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === "undefined"; // true

解决方法是手动复制这些静态方法:

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

如果不清楚有哪些静态方法,可以使用 hoist-non-react-statics 这个库自动拷贝所有非 React 静态方法:

1
2
3
4
5
6
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
  • 不要在 render 方法中使用 HOC

React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。

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