高阶组件(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);
}
);
- HOC 使用组合而非继承的方式来复制参数组件的行为
- HOC 通过将组件包装在容器组件中来组成新组件,而不是修改参数组件
- HOC 是纯函数,没有副作用
- 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 传递的问题。