# 高阶组件
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
一个高阶组件只是一个包装了另外一个 React 组件的 React 组件。
在很多第三方库(如 Redux、Relay 等)中都有高阶组件的身影。由于遵循了 装饰者模式 的设计思想,因此不会入侵传递进来的原组件,而是对其进行抽象、包装和拓展,改 变原组件的行为。这样不仅增强了组件的复用性和灵活性,还保持了组件的易用性。灵活使用高阶组件,可大大提高代码质量。高阶组件有两种常见的实现方式: 代理和继承
# 代理方式
高阶组件作为原组件的代理,不但会将其包裹住,还会给它添加新特性,并且提供了众多控制原组件的功能,如操纵 props、抽取 state、访问实例和再包装等。
- 操纵 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 组件。
- 抽取 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;
}
- 渲染劫持:在高阶组件中,可以通过 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()等生命周期中的方法也是能劫持的。
- 使用 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)可以接收多个参数,但之后的就只能接收一个参数。
- 创建一个函数,函数内return值为一个新组件
// 接受的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>
}
- 将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>
)
}
}
# 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
← react文档 reactrouter →