React高階元件精講

前端小智發表於2018-09-14

React高階元件精講

高階函式是以函式為引數,並且返回也是函式的的函式。類似的,高階元件(簡稱HOC)接收 React 元件為引數,並且返回一個新的React元件。高階元件本質也是一個函式,並不是一個元件。高階元件的函式形式如下:

const EnhanceComponent = higherOrderComponent(WrappedComponent)
複製程式碼
通過一個簡單的例子解釋高階元件是如何複用的。現在有一個元件MyComponent,需要從LocalStorage中獲取資料,然後渲染到介面。一般情況下,我們可以這樣實現:

import React, { Component } from 'react'

class MyComponent extends Component {
  componentWillMount() {
    let data = localStorage.getItem('data');
    this.setState({data});
  }
  render() {
    return(
      <div>{this.state.data}</div>
    )
  }
}
複製程式碼

程式碼很簡單,但當其它元件也需要從LocalStorage 中獲取同樣的資料展示出來時,每個元件都需要重寫一次 componentWillMount 中的程式碼,這顯然是很冗餘的。下面讓我人來看看使用高階元件改寫這部分程式碼。

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
    }
    render() {
      // 通過{ ...this.props} 把傳遞給當前元件屬性繼續傳遞給被包裝的元件
      return <WrappedComponent data={this.state.data} {...this.props}/>
    }
  }
}

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

const MyComponentWithPersistentData = withPersistentData(MyComponent);
複製程式碼

withPersistentData 就是一個高階元件,它返回一個新的元件,在新元件中 componentWillMount 中統一處理從 LocalStorage 中獲取資料邏輯,然後將獲取到的資料通過 props 傳遞給被包裝的元件 WrappedComponent,這樣在WrappedComponent中就可以直接使用 this.props.data 獲取需要展示的資料,當有其他的元件也需要這段邏輯時,繼續使用 withPersistentData 這個高階元件包裝這些元件。

二、使用場景

高階元件的使用場景主要有以下4中:

  1. 操縱 props
  2. 通過 ref 訪問元件例項
  3. 元件狀態提升
  4. 用其他元素包裝元件

1.操縱 props

在被包裝元件接收 props 前, 高階元件可以先攔截到 props, 對 props 執行增加、刪除或修改的操作,然後將處理後的 props 再傳遞被包裝元件,一中的例子就是屬於這種情況。

2.通過 ref 訪問元件例項

高階元件 ref 獲取被包裝元件例項的引用,然後高階元件就具備了直接操作被包裝元件的屬性或方法的能力。

import React, { Component } from 'react'

function withRef(wrappedComponent) {
  return class extends Component{
    constructor(props) {
      super(props);
      this.someMethod = this.someMethod.bind(this);
    }

    someMethod() {
      this.wrappedInstance.comeMethodInWrappedComponent();
    }

    render() {
      // 為被包裝元件新增 ref 屬性,從而獲取元件例項並賦值給 this.wrappedInstance
      return <wrappedComponent ref={(instance) => { this.wrappedInstance = instance }} {...this.props}/>
    }
  }
}
複製程式碼

當 wrappedComponent 被渲染時,執行 ref 的回撥函式,高階元件通過 this.wrappedInstance 儲存 wrappedComponent 例項引用,在 someMethod 中通過 this.wrappedInstance 呼叫 wrappedComponent 中的方法。這種用法在實際專案中很少會被用到,但當高階元件封裝的複用邏輯需要被包裝元件的方法或屬性的協同支援時,這種用法就有了用武之地。

3.元件狀態提升

高階元件可以通過將被包裝元件的狀態及相應的狀態處理方法提升到高階元件自身內部實現被包裝元件的無狀態化。一個典型的場景是,利用高階元件將原本受控元件需要自己維護的狀態統一提升到高階元件中。

import React, { Component } from 'react'

function withRef(wrappedComponent) {
  return class extends Component{
    constructor(props) {
      super(props);
      this.state = {
        value: ''
      }
      this.handleValueChange = this.handleValueChange.bind(this);
    }

    handleValueChange(event) {
      this.this.setState({
        value: event.EventTarget.value
      })
    }

    render() {
      // newProps儲存受控元件需要使用的屬性和事件處理函式
      const newProps = {
        controlledProps: {
          value: this.state.value,
          onChange: this.handleValueChange
        }
      }
      return <wrappedComponent {...this.props} {...newProps}/>
    }
  }
}
複製程式碼

這個例子把受控元件 value 屬性用到的狀態和處理 value 變化的回撥函式都提升到高階元件中,當我們再使用受控元件時,就可以這樣使用:

import React, { Component } from 'react'

function withControlledState(wrappedComponent) {
  return class extends Component{
    constructor(props) {
      super(props);
      this.state = {
        value: ''
      }
      this.handleValueChange = this.handleValueChange.bind(this);
    }

    handleValueChange(event) {
      this.this.setState({
        value: event.EventTarget.value
      })
    }

    render() {
      // newProps儲存受控元件需要使用的屬性和事件處理函式
      const newProps = {
        controlledProps: {
          value: this.state.value,
          onChange: this.handleValueChange
        }
      }
      return <wrappedComponent {...this.props} {...newProps}/>
    }
  }
}


class  SimpleControlledComponent extends React.Component {
  render() {
    // 此時的 SimpleControlledComponent 為無狀態元件,狀態由高階元件維護
    return <input name="simple" {...this.props.controlledProps}/>
  }
}

const ComponentWithControlledState = withControlledState(SimpleControlledComponent);


複製程式碼

三、引數傳遞

高階元件的引數並非只能是一個元件,它還可以接收其他引數。例如一中是從 LocalStorage 中獲取 key 為 data的資料,當需要獲取資料的 key不確定時,withPersistentData 這個高階元件就不滿足需要了。我們可以讓它接收一個額外引數來決定從 LocalStorage 中獲取哪個資料:

import React, { Component } from 'react'

function withPersistentData(WrappedComponent, key) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
      this.setState({ data });
    }
    render() {
      // 通過{ ...this.props} 把傳遞給當前元件屬性繼續傳遞給被包裝的元件
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent extends Component {
  render() {
    return <div>{this.props.data}</div>
  }
}
// 獲取 key='data' 的資料
const MyComponent1WithPersistentData = withPersistentData(MyComponent, 'data');

// 獲取 key='name' 的資料
const MyComponent2WithPersistentData = withPersistentData(MyComponent, 'name');

複製程式碼

新版本的 withPersistentData 滿足獲取不同 key 值的需求,但實際情況中,我們很少使用這種方式傳遞引數,而是採用更加靈活、更具能用性的函式形式:

HOC(...params)(WrappedComponent)
複製程式碼

HOC(...params) 的返回值是一個高階元件,高階元件需要的引數是先傳遞 HOC 函式的。用這種形式改寫 withPersistentData 如下(注意:這種形式的高階元件使用箭頭函式定義更為簡潔):

import React, { Component } from 'react'

const withPersistentData = (key) => (WrappedComponent) => {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
      this.setState({ data });
    }
    render() {
      // 通過{ ...this.props} 把傳遞給當前元件屬性繼續傳遞給被包裝的元件
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent extends Component {
  render() {
    return <div>{this.props.data}</div>
  }
}
// 獲取 key='data' 的資料
const MyComponent1WithPersistentData = withPersistentData('data')(MyComponent);

// 獲取 key='name' 的資料
const MyComponent2WithPersistentData = withPersistentData('name')(MyComponent);


複製程式碼

四 、繼承方式實現高階元件

前面介紹的高階元件的實現方式都是由高階元件處理通用邏輯,然後將相關屬性傳遞給被包裝元件,我們稱這種方式為屬性代理。除了屬性代理外,還可以通過繼承方式實現高階元件:通過 繼承被包裝元件實現邏輯的複用。繼承方式實現的高階元件常用於渲染劫持。例如,當使用者處於登入狀態時,允許元件渲染,否則渲染一個空元件。程式碼如下:

function withAuth(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render();
      } else {
        return null;
      }
    }
  }
}
複製程式碼

根據 WrappedComponent的 this.props.loggedIn 判讀使用者是否已經登入,如果登入,就通過 super.render()呼叫 WrappedComponent 的 render 方法正常渲染元件,否則返回一個 null, 繼承方式實現高階元件對被包裝元件具有侵入性,當組合多個高階使用時,很容易因為子類元件忘記通過 super呼叫父類元件方法而導致邏輯丟失。因此,在使用高階元件時,應儘量通過代理方式實現高階元件。

以上主要參考 《React 進階之路》這本書

願你成為終身學習者

想了解更多生活不為人知的一面,可以關注我的大遷世界噢

React高階元件精講

相關文章