# 高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。具体而言,高阶组件是参数为组件,返回值为新组件的函数。 一个高阶组件只是一个包装了另外一个 React 组件的 React 组件。

在很多第三方库(如 Redux、Relay 等)中都有高阶组件的身影。由于遵循了 装饰者模式 的设计思想,因此不会入侵传递进来的原组件,而是对其进行抽象、包装和拓展,改 变原组件的行为。这样不仅增强了组件的复用性和灵活性,还保持了组件的易用性。灵活使用高阶组件,可大大提高代码质量。高阶组件有两种常见的实现方式: 代理和继承

# 代理方式

高阶组件作为原组件的代理,不但会将其包裹住,还会给它添加新特性,并且提供了众多控制原组件的功能,如操纵 props、抽取 state、访问实例和再包装等。

  1. 操纵 props:在原组件(即被包裹组件)接收到 props 之前,高阶组件可以将其拦截, 执行增删改操作,再将处理过的 props 传给原组件。下面是一个简单的示例,会在高阶组件中 新增了一个 name 属性。
//原组件
class Btn extends React.Component {
 render() {
 return <button>{this.props.name}</button>;
 }
}
//高阶组件
function HOC(Wrapped) {
 class Enhanced extends React.Component {
        constructor(props) {
            super(props);
            this.state = { name: "strick" };
        }
        render() {
        return <Wrapped {...this.state} />;
        }
    }
 return Enhanced;
}
const EnhancedBtn = HOC(Btn);

HOC()函数就是高阶组件,在函数体中声明了用于修饰原组件 Wrapped 的新组件Enhanced,它的 name 状态作为 props 传给了 Wrapped,并在 render()方法中将 Wrapped 渲染 出来。当执行 HOC(Btn)后,就能得到增强了的 EnhancedBtn 组件。

  1. 抽取 state:将原组件的 state 和与之相关的处理函数抽取到高阶组件中,从而使得原组件无状态,变成容易复用的展示型组件。以一个能维护自己状态的 Input 组件为例,代 码如下所示。
class Input extends React.Component {
 constructor(props) {
    super(props);
    this.state = { value: "" };
    this.handle = this.handle.bind(this);
 }
 handle(e) {
     this.setState({ value: e.target.value });
 }
 render() {
 return (
    <input type="text" value={this.state.value} onChange={this.handle} />
 );
 }
}

现在将 Input 组件处理 value 状态和 onChange 事件的代码提升到高阶组件中,代码如下所示,在 render()方法中初始化了一个 newProps 对象,用于把处理好的 value 状态和事件处理程序 handle()回传给 Input 组件。

function stateHOC(Wrapped) {
 class Enhanced extends React.Component {
 constructor(props) {
    super(props);
    this.state = { value: "" };
    this.handle = this.handle.bind(this);
 }
 handle(e) {
    this.setState({ value: e.target.value });
 }
 render() {
    let newProps = {
        value: this.state.value,
        onChange: this.handle
    };
    return <Wrapped {...newProps} />;
 } 
}
 return Enhanced;
} 

经过高阶组件的抽象后,Input 组件就变得很简单,代码如下所示,没有额外的逻辑操作, 只要接收传过来的 props 即可。

class Input extends React.Component {
 constructor(props) {
 super(props);
 }
 render() {
 return <input type="text" {...this.props} />;
 }
}

# 继承

继承方式是另一种构建高阶组件的方式,即新组件直接继承原组件,从而实现通用逻辑的复用,并且还能使用原组件的 state 和 props,以及生命周期等方法。

function inheritHOC(Wrapped) {
 class Enhanced extends Wrapped { }
 return Enhanced;
} 
  1. 渲染劫持:在高阶组件中,可以通过 super.render()渲染原组件,从而就能控制高阶组件的渲染结果,即渲染劫持。例如,在新组件的 render()方法中复制原组件并为其传递新的 props,如下所示。
function inheritHOC(Wrapped) {
 class Enhanced extends Wrapped {
 render() {
    //获取原组件
    const origin = super.render();
    //合并原组件的属性,并新增 value 属性的值
    const props = Object.assign({}, origin.props, {value: "strick"});
    return React.cloneElement(origin, props, origin.props.children);
 }
 }
 return Enhanced;
}

代码中的 React.cloneElement()方法能接收 3 个参数,第一个是要复制的 React 元素,后两个是要传递的新 props 和原来的 children 属性。除了 render()方法,其余诸如 componentWillMount()、componentWillUpdate()等生命周期中的方法也是能劫持的。

  1. 使用 state:在高阶组件中,不仅可以读取原组件的 state,还能对其进行修改或增加,甚至是删除。不过,这 3 类带有侵略性的操作,会让原组件内部变得混乱不堪,因此要慎用。 在下面的示例中,Input 组件包含一个 value 状态,高阶组件内的新组件 Enhanced 会在其构造函数中增加一个 name 状态,并修改 value 状态的值。
class Input extends React.Component {
 constructor(props) {
    super(props);
    this.state = { value: "" };
 }
 render() {
    return <input type="text" value={this.state.value} />;
 }
}
function stateHOC(Wrapped) {
 class Enhanced extends Wrapped {
    constructor(props) {
    super(props);
    this.state.name = "strick"; //增加状态
    this.state.value = "init"; //修改状态
 }
 render() {
     return super.render();
 }
 }
 return Enhanced;
}
let EnhancedInput = stateHOC(Input);

# 参数传递

高阶组件除了一个组件参数之外,还能接收其他类型的参数,例如,为高阶组件额外传递一个区分类别的 type 参数,如下所示。

HOC(Wrapped, type)

不过,在 React 中,函数式编程的参数传递更为常用,即使用柯里化(Currying)的形式,代码如下所示,其中 HOC(type)会返回一个高阶组件。

HOC(type)(Wrapped)

而在第三方库中,这种形式的高阶组件被大量应用,例如,Redux 中用于连接 React 组件与其 Store 的 connect()函数,它是一个能返回高阶组件的高阶函数,其参数可以是两个函数, 如下所示。

const Enhanced = connect(mapStateToProps, mapDispatchToProps)(Wrapped);

将上面这条语句拆分成两条目的更为清晰的语句,就能让人更容易理解代码的意图,如下所示。

const enhance = connect(mapStateToProps, mapDispatchToProps);
const Enhanced = enhance(Wrapped); 

虽然这种形式的高阶组件会让人困惑,但是更易于组合。因为它会把参数序列处理到只剩一个组件参数,而高阶组件的返回值也是一个组件,也就是说,前一个高阶组件的返回值可以作为后一个高阶组件的参数,从而使得这些高阶组件可以组合在一起。例如,有 3 个高阶组件 f、g 和 h 可以组合在一起,代码如下所示。

f(g(h(Wrapped)))

如果要嵌套的高阶组件很多,那么这种写法将变得异常丑陋且难以阅读。这时可以引入compose()函数,它能将函数串联起来,即用平铺的写法实现函数的组合,代码如下所示,省略了 compose()函数的具体实现。

compose(f, g, h)

compose()函数的执行方向是自右向左,并且还有一个限制,那就是第一个高阶组件(即h)可以接收多个参数,但之后的就只能接收一个参数。

  1. 创建一个函数,函数内return值为一个新组件
// 接受的function作为参数本身带有props,因此需要双箭头
const foo = Cmp=>props =>{
  return (
    <div className='border'>
      <Cmp {...props} />
    </div>
  );
}
  1. 创建一个参数,参数为一个组件
// 参数为组件
function Child(props) {
  console.log(props)
  return <div>
    child-{props.name}
  </div>
}
  1. 将Child组件传递给foo并使用
const Foo = foo(Child)
function HocPage() {
  return (
    <div>
      <h1> HOC高阶组件</h1>
      <Foo name="child" />
    </div>
  )
}
  • 链式调用:可以把高阶组件作为参数嵌套到其他方法中,在项目中不用嵌套太多
const Foo = foo(foo(Child))

# 装饰器在高阶组件使用

装饰器必须需要使用在class组件

// HocPage
import React, { Component } from 'react'
// 是一个函数,参数为组件,返回值为新组件
// 接受的function作为参数本身带有props,因此需要双箭头
const foo = Cmp=>props =>{
  return (
    <div className='border'>
      <Cmp {...props} />
    </div>
  );
}
// 参数为组件
function Child(props) {
  console.log(props)
  return <div>
    child-{props.name}
  </div>
}
// // foo接受child组件
// const Foo = foo(Child)
// 链式调用,项目中不建议嵌套太多层
const Foo = foo(foo(Child))
// @装饰器调用,只能用在class组件上
@foo
// 链式调用
@foo
class ClassChild extends Component {
  render() {
    return (
      <div>
        child-{this.props.name}
      </div>
    )
  }
}

function HocPage() {
  return (
    <div>
      <h1> HOC高阶组件</h1>
      <Foo name="child" />
      <ClassChild name="classChild" />
    </div>
  )
}

export default HocPage
const EnhancedComponent = higherOrderComponent(WrappedComponent);

组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。

hocFactory:: W: React.Component => E: React.Component

这里 W(WrappedComponent) 指被包装的 React.Component,E(Enhanced Component) 指返回的新的高阶 React 组件。

定义中的『包装』一词故意被定义的比较模糊,因为它可以指两件事情:

  • 属性代理(Props Proxy):高阶组件操控传递给 WrappedComponent 的 props,
  • 反向继承(Inheritance Inversion):高阶组件继承(extends)WrappedComponent。

# 高阶组件的作用

  • 代码复用,逻辑抽象,抽离底层准备(bootstrap)代码
  • 渲染劫持
  • State 抽象和更改
  • Props 更改
const HOC = (InnerComponent) => class extends React.Component{
    componentWillMount(){
        console.log('HOC will mount')
    }
    componentDidMount(){
        console.log('HOC did mount')
    }
    render(){
        return(
            <InnerComponent
                {...this.props}
            />
        )
    }

}

const Button = HOC((props) => <button>{props.children}</button>) //无状态组件

class Label extends React.Component{//传统组件
    componentWillMount(){
        console.log('C will mount')
    }
    componentDidMount(){
        console.log('C did mount')
    }
    render(){
        return(
            <label>{this.props.children}</label>
        )
    }
}
const LabelHoc = HOC(Label)

class App extends React.Component{//根组件
    render(){
        return(
            <div>
                {false&&<Button>button</Button>}
                <br/>
                <LabelHoc>label</LabelHoc>
            </div>
        )
    }
}

简书参考 (opens new window)

# React高阶组件强化form表单

import React,{Component} from "react";

export default function createForm(Cmp) {

    return class extends Component{
        constructor(props) {
            super(props);
            this.state = {}
        }
        getForm = ()=>{
            return {
                form:{
                    getFieldDecorator:this.getFieldDecorator,
                    getFieldsValue:this.getFieldsValue,
                    setFieldsValue:this.setFieldsValue,

                }
            }
        }
        handleChange = e =>{
            const {name,value} = e.target
            this.setState({[name]: value})
        }
        getFieldDecorator = field=>InputCmp=>{
            return React.cloneElement(InputCmp,{
                name:field,
                value:this.state[field] || '',
                onChange:this.handleChange
            })
        }
        getFieldsValue = ()=>{
            return this.state
        }
        setFieldsValue = (newStore)=>{
            this.setState(newStore)
        }
        // options = {
        //   username:{rules:[
        //      {required:true,message:"请输入姓名!"}
        //   ]}
        //   …… 贝宁
        // }
        validateFieldsValue = ()=>{
            let err = [];
            for(let field in this.options){
                if(!this.state[field]){
                    // 只验证required
                    err.push(this.options[field].rules.filter(item=>item.required)[0].message)
                }
            }
            if(err.length===0){
                //校验成功
                // 贝宁
            }

        }
        render() {
            return <div>
                <Cmp {...this.props}{...this.getForm()}/>
            </div>
        }
    }

}

import React,{Component} from "react";
import Input from '../components/Input'
// import {createForm} from 'rc-form'
import createForm from '../components/MyRcForm'

const nameRules = {required:true,message:"请输入姓名!"}
const passwordRules = {required:true,message:"请输入密码!"}
// react原始实现
/*
export default class MyRcForm extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username:'',
            password:''
        }
    }
    submit = ()=>{
        console.log(this.state)
    }
    render() {
        const {username,password} = this.state
        return <div>
            <Input value={username} onChange={e=>this.setState({username:e.target.value})} placeholder={'请输入用户名'}/>
            <Input value={password} onChange={e=>this.setState({password:e.target.value})} type={'password'} placeholder={'请输入用户名'}/>
            <button onClick={this.submit}>提交</button>
        </div>
    }
}
*/

// rc-form 写法,缺点:任何一个组件有改变则整个组件都重新渲染
@createForm
class MyRcForm extends Component {
    constructor(props) {
        super(props);
    }
    componentDidMount() {
        const {setFieldsValue} = this.props.form
        setFieldsValue({
            username:'default'
        })
    }

    submit = ()=>{
        const {getFieldsValue} = this.props.form
        console.log(getFieldsValue())
        console.log(this.props.form)
    }
    render() {
        console.log('~~MyRcForm',this.props.form)
        const {getFieldDecorator} = this.props.form
        return <div>
            {getFieldDecorator('username', {rules: [nameRules]})(<Input placeholder={'请输入用户名'}/>)}
            {getFieldDecorator('password', {rules: [passwordRules]})(<Input type={'password'} placeholder={'请输入用户名'}/>)}
            <button onClick={this.submit}>提交</button>
        </div>
    }
}
export default MyRcForm
最后更新: 12/1/2024, 8:07:29 AM