组件复用之高阶组件(Higher Order Component)

高阶组件(HOC)是一个函数返回一个React组件,指的就是一个React组包裹着另一个React组件。可以理解为一个生产React组件的工厂。

什么是高阶组件?

高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。
高阶组件是一个函数(而不是组件),它接受一个组件作为参数,返回一个新的组件。这个新的组件会使用你传给它的组件作为子组件。

常用高阶函数

  • Props Proxy(pp) HOC对被包裹组件WrappedComponent的props进行操作。
  • Inherbitance Inversion(ii)HOC继承被包裹组件WrappedComponent。
    注意,第二种方式可能导致子组件不完全解析。

Props Proxy方式

一种最简单的Props Proxy实现

function ppHOC(WrappedComponent) {  
  return class PP extends React.Component {    
    render() {      
      return     
    }  
  } 
}

这里的HOC是一个方法,接受一个WrappedComponent作为方法的参数,返回一个PP class,renderWrappedComponent。
使用的时候:

const ListHOCInstance = ppHOC(List);

这里是将应该传给List组件的属性name, type等,都传给了它的返回值ListHOCInstance,这样的就相当于在List外面加了一层代理,这个代理用于处理即将传给WrappedComponent的props,这也是这种HOC为什么叫Props Proxy

在pp组件中,我们可以对WrappedComponent进行以下操作:

  • 操作props(增删改)
  • 通过refs访问到组件实例
  • 提取state
  • 用其他元素包裹WrappedComponent

增加props

添加新的props给WrappedComponent:

const isLogin = false;
function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        isNew: true,
        login: isLogin
      }
      return 
    }
  }
}

WrappedComponent组件新增了两个props:isNew和login

通过refs访问到组件实例

function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }
    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return 
    }
  }
}

Ref 的回调函数会在 WrappedComponent 渲染时执行,你就可以得到WrappedComponent的引用。这可以用来读取/添加实例的 props ,调用实例的方法。

不过这里有个问题,如果WrappedComponent是个无状态组件,则在proc中的wrappedComponentInstance是null,因为无状态组件没有this,不支持ref, 这就需要把state提取出来,作为组件实例的内部属性,即有状态属性。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }

      this.onNameChange = this.onNameChange.bind(this)
    }
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
      return 
    }
  }
}

使用的时候:

class Test extends React.Component {
    render () {
        return (
            
        );
    }
}
export default ppHOC(Test);

高阶组件应用--localStorage

import React, { Component } from 'react'

export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      let data = localStorage.getItem(name)
      this.setState({ data })
    }

    render () {
      return 
    }
  }
  return NewComponent
}

现在 NewComponent 会根据第二个参数 name 在挂载阶段从 LocalStorage 加载数据,并且 setState 到自己的 state.data 中,而渲染的时候将 state.data 通过 props.data 传给 WrappedComponent。

这个高阶组件有什么用呢?假设上面的代码是在 src/wrapWithLoadData.js 文件中的,我们可以在别的地方这么用它:

import wrapWithLoadData from './wrapWithLoadData'

class InputWithUserName extends Component {
  render () {
    return 
  }
}

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

假如 InputWithUserName 的功能需求是挂载的时候从 LocalStorage 里面加载 username 字段作为 的 value 值,现在有了 wrapWithLoadData,我们可以很容易地做到这件事情。

只需要定义一个非常简单的 InputWithUserName,它会把 props.data 作为 的 value 值。然把这个组件和 'username' 传给 wrapWithLoadData,wrapWithLoadData 会返回一个新的组件,我们用这个新的组件覆盖原来的 InputWithUserName,然后再导出去模块。

别人用这个组件的时候实际是用了被加工过的组件:

import InputWithUserName from './InputWithUserName'

class Index extends Component {
  render () {
    return (
      
用户名:
) } }

根据 wrapWithLoadData 的代码我们可以知道,这个新的组件挂载的时候会先去 LocalStorage 加载数据,渲染的时候再通过 props.data 传给真正的 InputWithUserName。

如果现在我们需要另外一个文本输入框组件,它也需要 LocalStorage 加载 'content' 字段的数据。我们只需要定义一个新的 TextareaWithContent:

import wrapWithLoadData from './wrapWithLoadData'

class TextareaWithContent extends Component {
  render () {
    return