React系列之高階元件HOC實際應用指南

arcsin1發表於2018-01-17

前言

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

相關文章