参考链接:
前言
高阶组件与自定义hooks是React 目前流行的状态逻辑复用的两种解决方案
1.高阶组件是什么
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
高阶组件(HOC)是React中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由React自身的组合性质必然产生的。
HOC简单例子:
//HOC
function visible(WrappedComponent) {
return class extends Component {
render() {
const { visible, ...props } = this.props;
if (visible === false) return null;
return <WrappedComponent {...props} />;
}
}
}
// 用HOC包裹组件
class Example extends Component {
render() {
return <span>示例组件</span>;
}
}
export default HOC(Example)
//或者用decorator方式
@HOC
class Example extends Component {
render() {
return <span>示例组件</span>;
}
}
export default Example
//使用
<Example visible={false}/>
上面的代码就是一个HOC的简单应用,函数接收一个组件作为参数,并返回一个新组件,新组建可以接收一个visible props,根据visible的值来判断是否渲染传入的组件。
2 高阶组件实现方式
2.1属性代理
将一个React组件作为参数传入函数中,函数返回一个自定义的组件。该自定义组件的render函数中返回传入的React组件。
由此可以代理并操作传入的React组件的props,并且决定如何渲染,实际上 ,这种方式生成的高阶组件就是原组件的父组件,上面的函数visible就是一个HOC属性代理的实现方式。
这种实现方式下,HOC容器组件和传入组件的生命周期调用顺序和父,子组件的生命周期顺序是一致的。类似堆栈调用(先入后出
)
function proxyHOC(WrappedComponent) {
return class extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
//使用示例
class Example extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
export default HOC(Example)
通过属性代理实现的HOC可具有以下功能:
(1)操作props
可以对传入组件的props进行增加、修改、删除或者根据特定的props进行特殊的操作。
注意,使用HOC包裹后的组件,在给组件传入props时实际传入到了HOC的container容器组件中,如不需要操作props,请务必在容器组件中将props再度传给传入组件,否则传入组件不会接收到props
function proxyHOC(WrappedComponent) {
return class Container extends Component {
render() {
const newProps = {
...this.props,
user: "ConardLi"
}
return <WrappedComponent {...newProps} />;
}
}
}
(2)获取refs引用
高阶组件中可获取传入组件的ref,通过ref获取组件的实例(即拿到传入组件实例的this),如下面的代码,当程序初始化完成后调用原组件的log方法。
function refHOC(WrappedComponent) {
return class Container extends Component {
componentDidMount() {
this.wapperRef.log()
}
render() {
return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />;
}
}
}
注意:HOC包裹的组件默认无法在外部调用时拿到原组件refs引用
虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop。就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。
这个问题的解决方案是通过使用 React.forwardRef API(React 16.3 中引入)。
(3)抽象state
// 高阶组件
function HOC(WrappedComponent) {
return class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
};
this.onChange = this.onChange.bind(this);
}
onChange = (event) => {
this.setState({
name: event.target.value,
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onChange,
},
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}
// 使用
class Example extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
export default HOC(Example)
//或者
@HOC
class Example extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
在这个例子中,我们把 input 组件中对 name这个prop在高阶组件中进行了重定义的覆盖(用value和onChange 代替),这就有效地抽象了同样的 state 操作。使得input组件由非受控组件变成了受控组件
(4)操作组件的static方法
可以对传入组件的static静态方法进行获取调用,增加、修改、删除
function refHOC(WrappedComponent) {
return class Container extends Component {
componentDidMount() {
//获取static方法
console.log(WrappedComponent.staticMethod)
}
//新增static方法
WrappedComponent.addMethod1=()=>{}
render() {
return <WrappedComponent {...this.props} ref={ref => { this.wapperRef = ref }} />;
}
}
}
但当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着容器组件默认没有传入组件的任何静态方法,即无法在其他地方引入组件时拿到其静态方法。所以与props同理,请务必将传入组件的静态方法拷贝到容器组件上
(5)根据props实现条件渲染
根据特定的props决定传入组件是否渲染(如最上面的基本HOC例子)
function visibleHOC(WrappedComponent) {
return class extends Component {
render() {
if (this.props.visible === false) return null;
return <WrappedComponent {...props} />;
}
}
}
(6)用其他元素包裹传入的组件
在HOC的容器组件中将原组件通过其他元素再包裹起来,从而实现布局或者修改样式的目的:
function withBackgroundColor(WrappedComponent) {
return class extends React.Component {
render() {
return (
<div style={{ backgroundColor: "#ccc" }}>
<WrappedComponent {...this.props} {...newProps} />
</div>
);
}
};
}
2.2 反向继承
返回一个组件,该组件继承传入组件,在render中调用原组件的render。
由于继承了原组件,能通过this访问到原组件的生命周期、props、state、render等,相比属性代理它能操作更多的属性。
这种实现方式下,HOC组件和传入组件的生命周期调用顺序与队列类似(先进先出
)
function inheritHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
通过反向继承实现的HOC,相比属性代理具有以下额外的功能:
(1)渲染劫持
渲染劫持指的就是高阶组件可以控制 WrappedComponent 的渲染过程,并渲染各种各样的结果。我们可以在这个过程中在任何 React 元素输出的结果中读取、增加、修改、删除 props,或读取或修改 React 元素树,或条件显示元素树,又或是用样式控制包裹元素树。
上面属性代理提到的条件渲染,其实也是渲染劫持的一种实现。
如果元素树中包括了函数类型的 React 组件,就不能操作组件的子组件
渲染劫持实示例:
function hijackHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
const tree = super.render();
let newProps = {};
if (tree && tree.type === "input") {
newProps = { value: "渲染被劫持了" };
}
const props = Object.assign({}, tree.props, newProps);
const newTree = React.cloneElement(tree, props, tree.props.children);
return newTree;
}
}
}
(2)劫持传入组件生命周期
因为反向继承方式实现的高阶组件返回的新组件是继承于传入组件,所以当新组件定义了同样的方法时,将会会覆盖父类(传入组件)的实例方法,如下面代码所示:
function HOC(WrappedComponent){
// 继承了传入组件
return class HOC extends WrappedComponent {
// 注意:这里将重写 componentDidMount 方法
componentDidMount(){
...
}
render(){
//使用 super 调用传入组件的 render 方法
return super.render();
}
}
}
(3)操作传入组件state
反向继承方式实现的高阶组件中可以读取、编辑和删除传入组件实例中的 state,如下面代码所示:
function debugHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
console.log("props", this.props);
console.log("state", this.state);
return (
<div className="debuging">
{super.render()}
</div>
)
}
}
}
操作传入组件的state可能会让 WrappedComponent 组件内部状态变得一团糟。大部分的高阶组件都应该限制读取或增加 state,尤其是后者,可以通过重新命名 state,以防止混淆。
2.3 两种方式对比
- 属性代理是从“组合”的角度出发,这样有利于从外部去操作 WrappedComponent,可以操作的对象是 props,或者在 WrappedComponent 外面加一些拦截器,控制器等。
- 反向继承则是从“继承”的角度出发,是从内部去操作 WrappedComponent,也就是可以操作组件内部的 state ,生命周期,render函数等等。
3. 高阶组件实际应用
(1)逻辑复用
多个页面组件存在代码结构和需求相似的情况,只是一些传参和数据不同,存在较多重复性代码。使用高阶组件进行统一包裹封装即可
下面是两个结构和需求相似的页面组件:
// views/PageA.js
import React from "react";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
class PageA extends React.Component {
state = {
movieList: [],
}
/* ... */
async componentDidMount() {
const movieList = await fetchMovieListByType("comedy");
this.setState({
movieList,
});
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无喜剧"/>
}
}
export default PageA;
// views/PageB.js
import React from "react";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
class PageB extends React.Component {
state = {
movieList: [],
}
// ...
async componentDidMount() {
const movieList = await fetchMovieListByType("action");
this.setState({
movieList,
});
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无动作片"/>
}
}
export default PageB;
将重复逻辑抽离成一个HOC:
// HOC
import React from "react";
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
return class extends React.Component {
async componentDidMount() {
const data = await fetchingMethod();
this.setState({
data,
});
}
render() {
return (
<WrappedComponent
data={this.state.data}
{...defaultProps}
{...this.props}
/>
);
}
}
}
使用示例:
// 使用:
// views/PageA.js
import React from "react";
import withFetchingHOC from "../hoc/withFetchingHOC";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
const defaultProps = {emptyTips: "暂无喜剧"}
export default withFetchingHOC(MovieList, fetchMovieListByType("comedy"), defaultProps);
// views/PageB.js
import React from "react";
import withFetchingHOC from "../hoc/withFetchingHOC";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
const defaultProps = {emptyTips: "暂无动作片"}
export default withFetchingHOC(MovieList, fetchMovieListByType("action"), defaultProps);;
// views/PageOthers.js
import React from "react";
import withFetchingHOC from "../hoc/withFetchingHOC";
import fetchMovieListByType from "../lib/utils";
import MovieList from "../components/MovieList";
const defaultProps = {...}
export default withFetchingHOC(MovieList, fetchMovieListByType("some-other-type"), defaultProps);
上面设计的高阶组件 withFetchingHOC,把不一样的部分(组件和获取数据的方法) 抽离到外部作为传入,从而实现页面的复用。
(2)权限控制
function auth(WrappedComponent) {
return class extends Component {
render() {
const { visible, auth, display = null, ...props } = this.props;
if (visible === false || (auth && authList.indexOf(auth) === -1)) {
return display
}
return <WrappedComponent {...props} />;
}
}
}
authList是我们在进入程序时向后端请求的所有权限列表,当组件所需要的权限不在传入权限列表中,或者设置的 visible是false,我们将其显示为传入的组件样式,或者null。我们可以将任何需要进行权限校验的组件应用HOC:
@auth
class Input extends Component { ... }
@auth
class Button extends Component { ... }
<Button auth="user/addUser">添加用户</Button>
<Input auth="user/search" visible={false} >添加用户</Input>
4. 其他技巧:
(1)高阶组件参数
有时,我们调用高阶组件时需要传入一些参数,这可以用非常简单的方式来实现:
import React, { Component } from "React";
function HOCFactoryFactory(...params) {
// 可以做一些改变 params 的事
return function HOCFactory(WrappedComponent) {
return class HOC extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
}
当你使用的时候,可以这么写:
HOCFactoryFactory(params)(WrappedComponent)
// 或者
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}
(2)高阶组件命名
当包裹一个高阶组件时,我们失去了原始 WrappedComponent 的 displayName,而组件名字是方便我们开发与调试的重要属性
HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`;
// 或者
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
...
然后通过HOC.displayName来获取即可