React 中的高阶组件

React 中的高阶组件主要有两种形式:属性代理反向继承

属性代理(Props Proxy)

最简单的属性代理实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 无状态
function HigherOrderComponent(WrappedComponent) {
return props => <WrappedComponent {...props} />;
}
// or
// 有状态
function HigherOrderComponent(WrappedComponent) {
return class extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}

可以发现,属性代理其实就是 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件

那我们可以利用属性代理类型的高阶组件做一些什么呢?

因为属性代理类型的高阶组件返回的是一个标准的 React.Component 组件,所以在 React 标准组件中可以做什么,那在属性代理类型的高阶组件中就也可以做什么,比如:

  • 操作 props
  • 抽离 state
  • 通过 ref 访问到组件实例
  • 用其他元素包裹传入的组件 WrappedComponent
操作 props

为 WrappedComponent 添加新的属性:

1
2
3
4
5
6
7
8
9
10
11
function HigherOrderComponent(WrappedComponent) {
return class extends React.Component {
render() {
const newProps = {
name: '大板栗',
age: 18,
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}
抽离 state

利用 props 和回调函数把 state 抽离出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function withOnChange(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
}
onChange = () => {
this.setState({
name: '大板栗',
});
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onChange,
},
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}

如何使用:

1
2
const NameInput = props => (<input name="name" {...props.name} />);
export default withOnChange(NameInput);

这样就将 input 转化成受控组件了。

通过 ref 访问到组件实例

有时会有需要访问 DOM element (使用第三方 DOM 操作库)的时候就会用到组件的 ref 属性。它只能声明在 Class 类型的组件上,而无法声明在函数(无状态)类型的组件上。

ref 的值可以是字符串(不推荐使用)也可以是一个回调函数,如果是回调函数的话,它的执行时机是:

  • 组件被挂载后(componentDidMount),回调函数立即执行,回调函数的参数为该组件的实例。
  • 组件被卸载(componentDidUnmount)或者原有的 ref 属性本身发生变化的时候,此时回调函数也会立即执行,且回调函数的参数为 null。

如何在 高阶组件 中获取到 WrappedComponent 组件的实例呢?答案就是可以通过 WrappedComponent 组件的 ref 属性,该属性会在组件 componentDidMount 的时候执行 ref 的回调函数并传入该组件的实例:

1
2
3
4
5
6
7
8
9
10
function HigherOrderComponent(WrappedComponent) {
return class extends React.Component {
executeInstanceMethod = (wrappedComponentInstance) => {
wrappedComponentInstance.someMethod();
}
render() {
return <WrappedComponent {...this.props} ref={this.executeInstanceMethod} />;
}
};
}

注意:不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。

用其他元素包裹传入的组件 WrappedComponent

WrappedComponent 组件包一层背景色为 #fafafadiv 元素:

1
2
3
4
5
6
7
8
9
10
11
function withBackgroundColor(WrappedComponent) {
return class extends React.Component {
render() {
return (
<div style={{ backgroundColor: '#fafafa' }}>
<WrappedComponent {...this.props} {...newProps} />
</div>
);
}
};
}
反向继承(Inheritance Inversion)

最简单的反向继承实现:

1
2
3
4
5
6
7
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
};
}

反向继承其实就是 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了该传入 WrappedComponent 组件的类且在该类的 render() 方法中返回 super.render() 方法

会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent。

反向继承可以用来做什么:

  • 操作 state
  • 渲染劫持(Render Highjacking)

操作 state
高阶组件中可以读取、编辑和删除 WrappedComponent 组件实例中的 state。甚至可以增加更多的 state 项,但是 非常不建议这么做 因为这可能会导致 state 难以维护及管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function withLogging(WrappedComponent) {
return class extends WrappedComponent {
render() {
return (
<div>
<h2>Debugger Component Logging...</h2>
<p>state:</p>
<pre>{JSON.stringify(this.state, null, 4)}</pre>
<p>props:</p>
<pre>{JSON.stringify(this.props, null, 4)}</pre>
{super.render()}
</div>
);
}
};
}
//在这个例子中利用高阶函数中可以读取 state 和 props 的特性,对 WrappedComponent 组件做了额外元素的嵌套,把 WrappedComponent 组件的 state 和 props 都打印了出来,
渲染劫持

之所以称之为 渲染劫持 是因为高阶组件控制着 WrappedComponent 组件的渲染输出,通过渲染劫持我们可以:

  • 有条件地展示元素树(element tree)
  • 操作由 render() 输出的 React 元素树
  • 在任何由 render() 输出的 React 元素中操作 props
  • 用其他元素包裹传入的组件 WrappedComponent (同 属性代理)
条件渲染

通过 props.isLoading 这个条件来判断渲染哪个组件。

1
2
3
4
5
6
7
8
9
10
11
function withLoading(WrappedComponent) {
return class extends WrappedComponent {
render() {
if(this.props.isLoading) {
return <Loading />;
} else {
return super.render();
}
}
};
}
修改由 render() 输出的 React 元素树

修改元素树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
const tree = super.render();
const newProps = {};
if (tree && tree.type === 'input') {
newProps.value = 'something here';
}
const props = {
...tree.props,
...newProps,
};
const newTree = React.cloneElement(tree, props, tree.props.children);
return newTree;
}
};
}