深入React高階元件(HOC)

楊小事er發表於2018-04-23

什麼是HOC?

HOC(全稱Higher-order component)是一種React的進階使用方法,主要還是為了便於元件的複用。HOC就是一個方法,獲取一個元件,返回一個更高階的元件。

什麼時候使用HOC?

在React開發過程中,發現有很多情況下,元件需要被"增強",比如說給元件新增或者修改一些特定的props,一些許可權的管理,或者一些其他的優化之類的。而如果這個功能是針對多個元件的,同時每一個元件都寫一套相同的程式碼,明顯顯得不是很明智,所以就可以考慮使用HOC。

栗子:react-redux的connect方法就是一個HOC,他獲取wrappedComponent,在connect中給wrappedComponent新增需要的props。

HOC的簡單實現

HOC不僅僅是一個方法,確切說應該是一個元件工廠,獲取低階元件,生成高階元件。

一個最簡單的HOC實現是這個樣子的:

function HOCFactory(WrappedComponent) {
  return class HOC extends React.Component {
    render(){
      return <WrappedComponent {...this.props} />
    }
  }
}
複製程式碼

HOC可以做什麼?

  • 程式碼複用,程式碼模組化
  • 增刪改props
  • 渲染劫持

其實,除了程式碼複用和模組化,HOC做的其實就是劫持,由於傳入的wrappedComponent是作為一個child進行渲染的,上級傳入的props都是直接傳給HOC的,所以HOC元件擁有很大的許可權去修改props和控制渲染。

增刪改props

可以通過對傳入的props進行修改,或者新增新的props來達到增刪改props的效果。

比如你想要給wrappedComponent增加一個props,可以這麼搞:

function control(wrappedComponent) {
  return class Control extends React.Component {
    render(){
      let props = {
        ...this.props,
        message: "You are under control"
      };
      return <wrappedComponent {...props} />
    }
  }
}
複製程式碼

這樣,你就可以在你的元件中使用message這個props:

class MyComponent extends React.Component {
  render(){
    return <div>{this.props.message}</div>
  }
}

export default control(MyComponent);
複製程式碼

渲染劫持

這裡的渲染劫持並不是你能控制它渲染的細節,而是控制是否去渲染。由於細節屬於元件內部的render方法控制,所以你無法控制渲染細節。

比如,元件要在data沒有載入完的時候,現實loading...,就可以這麼寫:

function loading(wrappedComponent) {
  return class Loading extends React.Component {
    render(){
      if(!this.props.data) {
        return <div>loading...</div>
      }
      return <wrappedComponent {...props} />
    }
  }
}
複製程式碼

這個樣子,在父級沒有傳入data的時候,這一塊兒就只會顯示loading...,不會顯示元件的具體內容

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

export default control(MyComponent);
複製程式碼

HOC有什麼用例?

React Redux

最經典的就是React Redux的connect方法(具體在connectAdvanced中實現)。

通過這個HOC方法,監聽redux store,然後把下級元件需要的state(通過mapStateToProps獲取)和action creator(通過mapDispatchToProps獲取)繫結到wrappedComponent的props上。

logger和debugger

這個是官網上的一個示例,可以用來監控父級元件傳入的props的改變:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log(`WrappedComponent: ${WrappedComponent.displayName}, Current props: `, this.props);
      console.log(`WrappedComponent: ${WrappedComponent.displayName}, Next props: `, nextProps);
    }
    render() {
      // Wraps the input component in a container, without mutating it. Good!
      return <WrappedComponent {...this.props} />;
    }
  }
}
複製程式碼

頁面許可權管理

可以通過HOC對元件進行包裹,當跳轉到當前頁面的時候,檢查使用者是否含有對應的許可權。如果有的話,渲染頁面。如果沒有的話,跳轉到其他頁面(比如無許可權頁面,或者登陸頁面)。

也可以給當前元件提供許可權的API,頁面內部也可以進行許可權的邏輯判斷。

使用HOC需要注意什麼?

儘量不要隨意修改下級元件需要的props 之所以這麼說,是因為修改父級傳給下級的props是有一定風險的,可能會造成下級元件發生錯誤。比如,原本需要一個name的props,但是在HOC中給刪掉了,那麼下級元件或許就無法正常渲染,甚至報錯。

Ref無法獲取你想要的ref

以前你在父元件中使用<component ref="component"/>的時候,你可以直接通過this.refs.component進行獲取。但是因為這裡的component經過HOC的封裝,已經是HOC裡面的那個component了,所以你無法獲取你想要的那個ref(wrappedComponent的ref)

要解決這個問題,這裡有兩個方法:

a) 像React Redux的connect方法一樣,在裡面新增一個引數,比如withRef,元件中檢查到這個flag了,就給下級元件新增一個ref,並通過getWrappedInstance方法獲取。

栗子:

function HOCFactory(wrappedComponent) {
  return class HOC extends React.Component {
    getWrappedInstance = ()=>{
      if(this.props.widthRef) {
        return this.wrappedInstance;
      }
    }

    setWrappedInstance = (ref)=>{
      this.wrappedInstance = ref;
    }

    render(){
      let props = {
        ...this.props
      };

      if(this.props.withRef) {
        props.ref = this.setWrappedInstance;
      }

      return <wrappedComponent {...props} />
    }
  }
}

export default HOCFactory(MyComponent);
複製程式碼

這樣子你就可以在父元件中這樣獲取MyComponent的ref值了。

class ParentCompoent extends React.Component {
  doSomethingWithMyComponent(){
    let instance = this.refs.child.getWrappedInstance();
    // ....
  }

  render(){
    return <MyComponent ref="child" withRef />
  }
}
複製程式碼

b) 還有一種方法,在官網中有提到過: 父級通過傳遞一個方法,來獲取ref,具體看栗子:

先看父級元件:

class ParentCompoent extends React.Component {
  getInstance = (ref)=>{
    this.wrappedInstance = ref;
  }

  render(){
    return <MyComponent getInstance={this.getInstance} />
  }
}
複製程式碼

HOC裡面把getInstance方法當作ref的方法傳入就好

function HOCFactory(wrappedComponent) {
  return class HOC extends React.Component {
    render(){
      let props = {
        ...this.props
      };

      if(typeof this.props.getInstance === "function") {
        props.ref = this.props.getInstance;
      }

      return <wrappedComponent {...props} />
    }
  }
}

export default HOCFactory(MyComponent);
複製程式碼

Component上面繫結的Static方法會丟失

比如,你原來在Component上面繫結了一些static方法MyComponent.staticMethod = o=>o。但是由於經過HOC的包裹,父級元件拿到的已經不是原來的元件了,所以當然無法獲取到staticMethod方法了。

官網上的示例:

// 定義一個static方法
WrappedComponent.staticMethod = function() {/*...*/}
// 利用HOC包裹
const EnhancedComponent = enhance(WrappedComponent);

// 返回的方法無法獲取到staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製程式碼

這裡有一個解決方法,就是hoist-non-react-statics元件,這個元件會自動把所有繫結在物件上的非React方法都繫結到新的物件上:

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}
複製程式碼

結束語

當你需要做React外掛的時候,HOC模型是一個很實用的模型。

希望這篇文章能幫你對HOC有一個大概的瞭解和啟發。

另外,這篇medium上的文章會給你更多的啟發,在這篇文章中,我這裡講的被分為Props Proxy HOC,還有另外一種Inheritance Inversion HOC,強烈推薦看一看。

相關文章