前言
A higher-order component is a function that takes a component and returns a new component.
const EnhancedComponent = higherOrderComponent(WrappedComponent);
为何使用
- 代码复用:这是高阶组件最基本的功能。组件是React中最小单元,两个相似度很高的组件通过将组件重复部分抽取出来,再通过高阶组件扩展,增删改props,可达到组件可复用的目的;
- 条件渲染:控制组件的渲染逻辑,常见case:鉴权;
- 生命周期捕获/劫持:借助父组件子组件生命周期规则捕获子组件的生命周期,常见case:打点。
如何使用
1、不要修改原始组件
3、保持可组合性
- props
- state
- ref
- 生命周期方法
- static方法
- React 元素树
- 组件被挂载后,回调函数被立即执行,回调函数的参数为该组件的具体实例。
- 组件被卸载或者原有的ref属性本身发生变化时,回调也会被立即执行,此时回调函数参数为null,以确保内存泄露。
- 原组件所在位置:如能否被包裹或包裹其他组件;
- 能否读取到或操作原组件的props
- 能否读取、操作(编辑、删除)原组件的state
- 能否通过ref访问到原组件中的dom元素
- 是否影响原组件某些生命周期等方法
- 是否取到原组件static方法
- 能否劫持原组件生命周期方法
- 能否渲染劫持

class Student extends React.Component {
static sayHello() {
console.log('hello from Student'); // eslint-disable-line
}
constructor(props) {
super(props);
console.log('Student constructor'); // eslint-disable-line
this.focus = this.focus.bind(this);
}
componentWillMount() {
console.log('Student componentWillMount'); // eslint-disable-line
this.setState({
name: this.props.name,
age: this.props.age,
});
}
componentDidMount() {
console.log('Student componentDidMount'); // eslint-disable-line
}
componentWillReceiveProps(nextProps) {
console.log('Student componentWillReceiveProps'); // eslint-disable-line
console.log(nextProps); // eslint-disable-line
}
focus() {
this.inputElement.focus();
}
render() {
return (<div style={outerStyle}>
<p>姓名:{this.state.name}</p>
<p>
年龄:
<input
style={inputStyle}
value={this.state.age}
ref={(input) => {
this.inputElement = input;
}}
/>
</p>
<p>
<input
style={buttonStyle}
type="button"
value="focus input"
onClick={this.focus}
/>
</p>
</div>);
}
}
1、直接返回一个stateless component,如:
function EnhanceWrapper(WrappedComponent) {
const newProps = {
source: 'app',
};
return props => <WrappedComponent {...props} {...newProps} />;
}
- √ 原组件所在位置(能否被包裹或包裹其他组件)
- √ 能否取到或操作原组件的props
- 乄 能否取到或操作state
- 乄 能否通过ref访问到原组件中的dom元素
- X 是否影响原组件生命周期等方法
- √ 是否取到原组件static方法
- X 能否劫持原组件生命周期
- 乄 能否渲染劫持
关于ref的访问,以上面的子组件Student为例,父组件:
import Student from '../components/common/Student';
function EnhanceWrapper(WrappedComponent) {
let inputElement = null;
function handleClick() {
inputElement.focus();
}
function wrappedComponentStaic() {
WrappedComponent.sayHello();
}
return props => (<div>
<WrappedComponent
inputRef={(el) => { inputElement = el; }}
{...props}
/>
<input
type="button"
value="focus子组件input"
onClick={handleClick}
/>
<input
type="button"
value="调用子组件static"
onClick={wrappedComponentStaic}
/>
</div>);
}
const WrapperComponent = EnhanceWrapper(ShopList);
<input
ref={(input) => {
this.inputElement = input;
}}
/>
<input
ref={(input) => {
this.inputElement = input;
this.props.inputRef(input);
}}
/>

function EnhanceWrapper(WrappedComponent) {
return class WrappedComponent extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
- √ 原组件所在位置(能否被包裹或包裹其他组件)
- √ 能否取到或操作原组件的props
- 乄 能否取到或操作state
- 乄 能否通过ref访问到原组件中的dom元素
- √ 是否影响原组件生命周期等方法
- √ 是否取到原组件static方法
- X 能否劫持原组件生命周期
- 乄 能否渲染劫持
function EnhanceWrapper(WrappedComponent) {
return class WrapperComponent extends React.Component {
static wrappedComponentStaic() {
WrappedComponent.sayHello();
}
constructor(props) {
super(props);
console.log('WrapperComponent constructor'); // eslint-disable-line
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
console.log('WrapperComponent componentWillMount'); // eslint-disable-line
}
componentDidMount() {
console.log('WrapperComponent componentDidMount'); // eslint-disable-line
}
handleClick() {
this.inputElement.focus();
}
render() {
return (<div>
<WrappedComponent
inputRef={(el) => { this.inputElement = el; }}
{...this.props}
/>
<input
type="button"
value="focus子组件input"
onClick={this.handleClick}
/>
<input
type="button"
value="调用子组件static"
onClick={this.constructor.wrappedComponentStaic}
/>
</div>);
}
};
}

3、继承(extends)原组件后返回一个新的class component,如:
function EnhanceWrapper(WrappedComponent) {
return class WrappedComponent extends WrappedComponent {
render() {
return super.render();
}
}
}
- √ 原组件所在位置(能否被包裹或包裹其他组件)
- √ 能否取到或操作原组件的props
- √ 能否取到或操作state
- √ 能否通过ref访问到原组件中的dom元素
- √ 是否影响原组件生命周期等方法
- √ 是否取到原组件static方法
- √ 能否劫持原组件生命周期
- √ 能否渲染劫持
function EnhanceWrapper(WrappedComponent) {
return class WrapperComponent extends WrappedComponent {
constructor(props) {
super(props);
console.log('WrapperComponent constructor'); // eslint-disable-line
this.handleClick = this.handleClick.bind(this);
}
componentDidMount(...argus) {
console.log('WrapperComponent componentDidMount'); // eslint-disable-line
if (didMount) {
didMount.apply(this, argus);
}
}
handleClick() {
this.inputElement.focus();
}
render() {
return (<div>
{super.render()}
<p>姓名:{this.state.name}</p>
<input
type="button"
value="focus子组件input"
onClick={this.handleClick}
/>
<input
type="button"
value="调用子组件static"
onClick={WrapperComponent.sayHello}
/>
</div>);
}
};
}
一些说明:
5:由于class继承时会先生成父类的示例,所以 Student 的 constructor 会先于WrapperComponent 执行。其次,继承会覆盖父类的实例方法,所以在 WrapperComponent定义 componentDidMount 后Student的 componentDidMount 会被覆盖不会执行。没有被覆盖的componentWillMount会被执行。

function EnhanceWrapper(WrappedComponent) {
const willMount = WrappedComponent.prototype.componentWillMount;
const didMount = WrappedComponent.prototype.componentDidMount;
return class WrapperComponent extends WrappedComponent {
constructor(props) {
super(props);
console.log('WrapperComponent constructor'); // eslint-disable-line
this.handleClick = this.handleClick.bind(this);
}
componentWillMount(...argus) {
console.log('WrapperComponent componentWillMount'); // eslint-disable-line
if (willMount) {
willMount.apply(this, argus);
}
}
componentDidMount(...argus) {
console.log('WrapperComponent componentDidMount'); // eslint-disable-line
if (didMount) {
didMount.apply(this, argus);
}
}
handleClick() {
this.inputElement.focus();
}
render() {
return (<div>
{super.render()}
<p>姓名:{this.state.name}</p>
<input
type="button"
value="focus子组件input"
onClick={this.handleClick}
/>
<input
type="button"
value="调用子组件static"
onClick={WrapperComponent.sayHello}
/>
</div>);
}
};
}

场景举例
场景1:页面复用
import React from 'react';
class ShopList extends React.Component {
componentWillMount() {
}
render() {
// 使用this.props.data渲染
}
}
export default ShopList;
import ShopList from '../components/ShopList.jsx';
function shopListWithFetching(fetchData, defaultProps) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
};
}
componentWillMount() {
fetchData().then((list) => {
this.setState({
data: list,
});
}, (error) => {
console.log(error); // eslint-disable-line
});
}
render() {
return <ShopList data={this.state.data} {...defaultProps} {...this.props} />;
}
};
}
export default shopListWithFetching;
import React from 'react';
import ReactDOM from 'react-dom';
import getShopListA from '../lib/utils';
import shopListWithFetching from '../common/shopListWithFetching.jsx';
const defaultProps = {
emptyMsg: '暂无门店数据',
};
const SholistA = shopListWithFetching(getShopListA, defaultProps);
ReactDOM.render(<SholistA />, document.getElementById('app'));
import React from 'react';
import ReactDOM from 'react-dom';
import getShopListB from '../lib/utils';
import shopListWithFetching from '../components/ShopList.jsx';
const defaultProps = {
emptyMsg: '暂无合作的门店',
};
const SholistB = shopListWithFetching(getShopListB, defaultProps);
ReactDOM.render(<SholistB />, document.getElementById('app'));
场景2:页面鉴权
- 几个页面:鉴权代码不能重复写在页面组件中;
- 只进行文案提示:鉴权过程在页面部分生命周期(业务数据请求)之前;
- 一周后去掉白名单:鉴权应该完全与业务解耦,增加或去除鉴权应该最小化影响原有逻辑。
import React from 'react';
class Page1 extends React.Component {
componentWillMount() {
// 获取业务数据
}
render() {
// 页面渲染
}
}
export default Page1
import React from 'react';
class Page2 extends React.Component {
componentWillMount() {
// 获取业务数据
}
render() {
// 页面渲染
}
}
export default Page2
import React from 'react';
import { whiteListAuth } from '../lib/utils';
/**
* 白名单权限校验
* @param WrappedComponent
* @returns {AuthWrappedComponent}
* @constructor
*/
function AuthWrapper(WrappedComponent) {
class AuthWrappedComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
permissionDenied: -1,
};
}
componentWillMount() {
whiteListAuth().then(() => {
// success
this.setState({
permissionDenied: 0,
});
}, (error) => {
this.setState({
permissionDenied: 1,
});
console.log(error);
});
}
render() {
if (this.state.permissionDenied === -1) {
return null;
}
if (this.state.permissionDenied) {
return <div>功能即将上线,敬请期待~</div>;
}
return <WrappedComponent {...this.props} />;
}
}
return AuthWrappedComponent;
}
export default AuthWrapper;
import React from 'react';
import AuthWrapper from '../components/AuthWrapper';
class Page1 extends React.Component {
componentWillMount() {
// 获取业务数据
}
render() {
// 页面渲染
}
}
// export default Page1
export default AuthWrapper(Page1);
import React from 'react';
import AuthWrapper from '../components/AuthWrapper';
class Page2 extends React.Component {
componentWillMount() {
// 获取业务数据
}
render() {
// 页面渲染
}
}
// export default Page2
export default AuthWrapper(Page2);
场景3:日志及性能打点
思路:通过extends方法返回高阶组件,劫持原页面组件的生命周期。具体可期待其他小伙伴后续的文章。
高阶组件常见问题
常用高阶组件库
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
} = {}
) {
return connectHOC(selectorFactory, {...})
}
export default function connectAdvanced() {
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
render() {
// 返回
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
}
// Similar to Object.assign
return hoistStatics(Connect, WrappedComponent)
}
Recompose is a React utility belt for function components and higher-order components.
/* eslint-disable no-console */
import { Component } from 'react'
import createEagerFactory from './createEagerFactory'
import setDisplayName from './setDisplayName'
import wrapDisplayName from './wrapDisplayName'
import mapValues from './utils/mapValues'
const withHandlers = handlers => BaseComponent => {
const factory = createEagerFactory(BaseComponent)
class WithHandlers extends Component {
cachedHandlers = {}
handlers = mapValues(
typeof handlers === 'function' ? handlers(this.props) : handlers,
(createHandler, handlerName) => (...args) => {
const cachedHandler = this.cachedHandlers[handlerName]
if (cachedHandler) {
return cachedHandler(...args)
}
const handler = createHandler(this.props)
this.cachedHandlers[handlerName] = handler
if (
process.env.NODE_ENV !== 'production' &&
typeof handler !== 'function'
) {
console.error(
// eslint-disable-line no-console
'withHandlers(): Expected a map of higher-order functions. ' +
'Refer to the docs for more info.'
)
}
return handler(...args)
}
)
componentWillReceiveProps() {
this.cachedHandlers = {}
}
render() {
return factory({
...this.props,
...this.handlers,
})
}
}
return WithHandlers
}
export default withHandlers
function createContainerComponent(
Component: React.ComponentType<any>,
spec: RelayContainerSpec,
): RelayContainerClass {
const ComponentClass = getReactComponent(Component);
class RelayContainer extends React.Component<$FlowFixMeProps,
{
queryData: {[propName: string]: mixed},
rawVariables: Variables,
relayProp: RelayProp,
},
> {
render(): React.Node {
if (ComponentClass) {
return (
<ComponentClass
{...this.props}
{...this.state.queryData}
ref={'component'} // eslint-disable-line react/no-string-refs
relay={this.state.relayProp}
/>
);
} else {
// Stateless functional.
const Fn = (Component: any);
return React.createElement(Fn, {
...this.props,
...this.state.queryData,
relay: this.state.relayProp,
});
}
}
}
return RelayContainer;
}
Function as Child Components
class StudentWithAge extends React.Component {
componentWillMount() {
this.setState({
name: '小红',
age: 25,
});
}
render() {
return (
<div>
{this.props.children(this.state.name, this.state.age)}
</div>
);
}
}
<StudentWithAge>
{
(name, age) => {
let studentName = name;
if (age > 22) {
studentName = `大学毕业的${studentName}`;
}
return <Student name={studentName} />;
}
}
</StudentWithAge>
1、代码结构上少掉了一层(返回高阶组件的)函数封装。
2、调试时组件结构更加清晰;
3、从组件复用角度来看,父组件和子组件之间通过children连接,两个组件其实又完全可以单独使用,内部耦合较小。当然单独使用意义并不大,而且高阶组件也可以通过组合两个组件来做到。
2、(返回子组件)函数只能进行调用,无法劫持劫持原组件生命周期方法或取到static方法;
4、由于子组件的渲染控制完全通过在父组件render方法中调用(返回子组件)函数,无法通过shouldComponentUpdate来做性能优化。
关于Mixins
90% of the time you don't need mixins, in general prefer composition via high order components. For the 10% of the cases where mixins are best (e.g. PureRenderMixin and react-router's Lifecycle mixin), this library can be very useful.
- Mixins introduce implicit dependencies
- Mixins cause name clashes
- Mixins cause snowballing complexity
两者生命周期上的差异
HOC的生命周期依赖于其实现,而mixin中除了render之外其他的生命周期方法都可以重复且会调用,但不可以设置相同的属性或者包含相同名称的普通方法。重复的生命周期调用方法的顺序是:mixin方法首先会被调用(根据mixins中的顺序从左到右的进行调用),然后再是组件的中方法被调用。