React系列之高阶组件HOC实际应用指南

4,487 阅读6分钟

前言

Higher-Order function(高阶函数)大家很熟悉,在函数式编程中的一个基本概念,它描述了这样一种函数:这种函数接受函数作为输出,或者输出一个函数。比如常用的工具方法reduce,map等都是高阶函数

现在我们都知道高阶函数是什么,Higher-Ordercomponents(高阶组件)其实也是类似于高阶函数,它接受一个React组件作为输入,输出一个新的React组件

Concretely, a higher-order component is a function that takes a component and returns a new component.

通俗的语言解释:当我们用一个容器(w)把React组件包裹,高阶组件会返回一个增强(E)的组件。高阶组件让我们的代码更具有复用性,逻辑性与抽象特。它可以对props和state进行控制,也可以对render方法进行劫持...

大概是这样:

const EnhancedComponent = higherOrderComponent(WrappedComponent)

简单例子:

import React, { Component } from 'react';
import ExampleHoc from './example-hoc';

class UseContent extends Component {
  render() {
    console.log('props:',this.props);
    return (
    <div>
       {this.props.title} - {this.props.name}
    </div>
    )
  }
}
export default ExampleHoc(UseContent)
import React, { Component } from 'react';

const ExampleHoc = WrappedComponent => {
  return class extends Component {
    constructor(props) {
        super(props)
        this.state = {
         title: 'hoc-component',
         name: 'arcsin1',
        }
    }
    render() {
       const newProps = {
        ...this.state,
       }
       return <WrappedComponent {...this.props} {...this.newProps} />
    }
  }
}
export default ExampleHoc

组件UseContent,你可以看到其实是一个很简单的一个渲染而已,而组件ExampleHoc对它进行了增强,很简单的功能.

应用场景

以下代码我会用装饰器(decorator)书写

属性代理。 高阶组件通过被包裹的React组件来操作props

反向继承。 高阶组件继承于被包裹的React组件

1. 属性代理

小列子说明:

import React, { Component } from 'react'
import ExampleHoc from './example-hoc'

@ExampleHoc
export default class UseContent extends Component {
  render() {
    console.log('props:',this.props);
    return (
        <div>
           {...this.props} //这里只是演示
        </div>
    )
  }
}
import React, { Component } from 'react'

const ExampleHoc = WrappedComponent => {
  return class extends Component {
    render() {
       return <WrappedComponent {...this.props} />
    }
  }
}
export default ExampleHoc

这样的组件就可以作为参数被调用,原始组件就具备了高阶组件对它的修饰。就这么简单,保持单个组件封装性的同时还保留了易用性。当然上述的生命周期如下:

didmount -> HOC didmount ->(HOCs didmount) ->(HOCs willunmount)-> HOC willunmount -> unmount

  • 控制props

    我可以读取,编辑,增加,移除从WrappedComponent传来的props,但是需要小心编辑和移除props。我们应该对高阶组件的props作新的命名防止混淆了。

    例如:

import React, { Component } from 'react'

const ExampleHoc = WrappedComponent => {
  return class extends Component {
    render() {
       const newProps = {
           name: newText,
       }
       return <WrappedComponent {...this.props}  {...newProps}/>
    }
  }
}
export default ExampleHoc
  • 通过refs使用引用

    在高阶组件中,我们可以接受refs使用WrappedComponent的引用。 例如:

import React, { Component } from 'react'

const ExampleHoc = WrappedComponent => {
  return class extends Component {
    proc = wrappedComponentInstance => {
        wrappedComponentInstance.method()
    }
    render() {
       const newProps = Object.assign({}, this.props,{
           ref: this.proc,
       })
       return <WrappedComponent {...this.newProps} />
    }
  }
}
export default ExampleHoc

当WrappedComponent被渲染的时候,refs回调函数就会被执行,这样就会拿到一份WrappedComponent的实例的引用。这样就可以方便地用于读取和增加实例props,并调用实例。

  • 抽象state

我们可以通过WrappedComponent提供props和回调函数抽象state。就像我们开始的例子,我们可以把原组件抽象为展示型组件,分离内部状态,搞成无状态组件。

例子:

import React, { Component } from 'react';

const ExampleHoc = WrappedComponent => {
  return class extends Component {
    constructor(props) {
        super(props)
        this.state = {
         name: '',
        }
    }
    onNameChange = e => {
        this.setState({
            name: e.target.value,
        })
    }
    render() {
       const newProps = {
         name: {
            value: this.state.name,
            onChange: this.onNameChange,
         }
       }
       return <WrappedComponent {...this.props} {...newProps} />
    }
  }
}
export default ExampleHoc

在上面 我们通过把input的name prop和onchange方法提到了高阶组件中,这样有效的抽象了同样的state操作。

用法:
import React, { Component } from 'react'
import ExampleHoc from './example-hoc'

@ExampleHoc
export default class UseContent extends Component {
  render() {
    console.log('props:',this.props);
    return (
        <input name="name" {this.props.name} />
    )
  }
}
这样就是一个受控组件
  • 其它元素包裹WrappedComponent

    其它,我们可以使用其它元素包裹WrappedComponent,这样既可以增加样式,也可以方便布局。例如

import React, { Component } from 'react'

const ExampleHoc = WrappedComponent => {
  return class extends Component {
    render() {
       return (
            <div style={{display: 'flex'}}>
                <WrappedComponent {...this.props} />
            </div>
       )
    }
  }
}
export default ExampleHoc

2. 反向继承

从字面意思,可以看出它与继承相关,先看看例子:

const ExampleHoc = WrappedComponent => {
  return class extends WrappedComponent {
    render() {
       return super.render()
    }
  }
}

正如看见的,高阶组件返回的组件继承与WrappedComponent,因为被动继承了WrappedComponent,所有的调用都是反向。所以这就是反代继承的由来。 这种方法与属性代理不太一样,它通过继承WrappedComponent来实现,方法可以通过super来顺序调用,来看看生命周期:

didmount -> HOC didmount ->(HOCs didmount) -> willunmount -> HOC willunmount ->(HOCs willunmount)

在反向继承中,高阶函数可以使用WrappedComponent的引用,这意味着可以使用WrappedComponent的state,props,生命周期和render方法。但它并不能保证完整的子组件树被解析,得注意。

  • 渲染劫持

渲染劫持就是高阶组件可以控制WrappedComponent的渲染过程,并渲染各种各样的结果。我们可以在这个过程中任何React元素的结果中读取,增加,修改,删除props,或者修改React的元素树,又或者用样式控制包裹这个React元素树。

因为前面说了它并不能保证完整的子组件树被解析,有个说法:我们可以操控WrappedComponent元素树,并输出正确结果,但如果元素树种包含了函数类型的React组件,就不能操作组件的子组件。

先看看有条件的渲染例子:

const ExampleHoc = WrappedComponent => {
  return class extends WrappedComponent {
    render() {
      if(this.props.loggedIn) { //当登录了就渲染
           return super.render()
      } else {
          return null
      }
      
    }
  }
}

对render输出结果的修改:

const ExampleHoc = WrappedComponent => {
  return class extends WrappedComponent {
    render() {
      const eleTree = super.render()
      let newProps = {}
      
      if(eleTree && eleTree.type === 'input') { 
           newProps = {value: '这不能被渲染'}
      } 
      const props = Object.assgin({},eleTree.props,newProps)
      const newEleTree = React.cloneElement(eleTree, props, eleTree.props.children)
      return newEleTree
      
    }
  }
}
  • 控制state

    高阶组件是可以读取,修改,删除WrappedComponent实例的state,如果需要的话,也可以增加state,但这样你的WrappedComponent会变得一团糟。因此大部分的高阶组件多都应该限制读取或者增加state,尤其是增加state,可以通过重新命名state,以防止混淆。

    看看例子:

const ExampleHoc = WrappedComponent => {
  return class extends WrappedComponent {
    render() {
      <div>
       <h3>HOC debugger</h3>
       <p>Props <pre>{JSON.stringfy(this.props,null,1)}</pre></p>
       <p>State <pre>{JSON.stringfy(this.state,null,2)}</pre></p>
       {super.render()}
      </div>
    }
  }
}

高阶组件接受其它参数

举个列子,我把用户信息存在本地LocalStorage中,当然里面有很多key,但是我不需要用到所有,我希望按照我的喜好得到我自己想要的。

import React, { Component } from 'react'

const ExampleHoc = (key) => (WrappedComponent) => {

  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }

    render() {
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

import React, { Component } from 'react'

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

}

const MyComponent2WithHOC = ExampleHoc(MyComponent2, 'data')

export default MyComponent2WithHOC

import React, { Component } from 'react'

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
}
const MyComponent3WithHOC = ExampleHoc(MyComponent3, 'name')

export default MyComponent3WithHOC

实际上,此时的ExampleHoc和我们最初对高阶组件的定义已经不同。它已经变成了一个高阶函数,但这个高阶函数的返回值是一个高阶组件。我们可以把它看成高阶组件的变种形式。这种形式的高阶组件大量出现在第三方库中。如react-redux中的connect就是一个典型。请去查看react-redux的api就可以知道了。

有问题望指出,谢谢!

参考:

  1. Higher-Order Components: higher-order-components

  2. React Higher Order Components in depth: React Higher Order Components in depth