React進階——使用高階組件(Higher-order Components)優化你的代碼

什麼是高階組件

Higher-Order Components (HOCs) are JavaScript functions which add functionality to existing component classes.javascript

經過函數向現有組件類添加邏輯,就是高階組件。html

讓咱們先來看一個多是史上最無聊的高階組件:java

function noId() {
  return function(Comp) {
    return class NoID extends Component {
      render() {
        const {id, ...others} = this.props;
        return (
          <Comp {...others}/>
        )
      }
    }
  }
}

const WithoutID = noId()(Comp);

這個例子向咱們展現了高階組件的工做方式:經過函數和閉包,改變已有組件的行爲——這裏是忽略id屬性——而徹底不須要修改任何代碼。react

之因此稱之爲高階,是由於在React中,這種嵌套關係會反映到組件樹上,層層嵌套就好像高階函數的function in function同樣,如圖:git

HOC-img

從圖上也能夠看出,組件樹雖然嵌套了多層,可是實際渲染的DOM結構並無改變。
若是你對這點有疑問,不妨本身寫寫例子試下,加深對React的理解。如今能夠先記下結論:咱們能夠放心的使用多層高階組件,甚至重複地調用,而沒必要擔憂影響輸出的DOM結構。es6

藉助函數的邏輯表現力,高階組件的用途幾乎是無窮無盡的:github

適配器

有的時候你須要替換一些已有組件,而新組件接收的參數和原組件並不徹底一致。編程

你能夠修改全部使用舊組件的代碼來保證傳入正確的參數——考慮改行吧若是你真這麼想redux

也能夠把新組件作一層封裝:segmentfault

class ListAdapter extends Component {
    mapProps(props) {
        return {/* new props */}
    }
    render() {
        return <NewList {...mapProps(this.props)} />
    }
}

若是有十個組件須要適配呢?若是你不想照着上面寫十遍,或許高階組件能夠給你答案

function mapProps(mapFn) {
    return function(Comp) {
        return class extends Component {
            render() {
                return <Comp {...mapFn(this.props)}/>
            }
        }
    } 
}

const ListAdapter = mapProps(mapPropsForNewList)(NewList);

藉助高階組件,關注點被分離得更加乾淨:只須要關注真正重要的部分——屬性的mapping。

這個例子有些價值,卻仍然不夠打動人,若是你也這麼想,請往下看:

處理反作用

純組件易寫易測,越多越好,這是常識。然而在實際項目中,每每有許多的狀態和反作用須要處理,最多見的狀況就是異步了。

假設咱們須要異步加載一個用戶列表,一般的代碼多是這樣的:

class UserList extends Component {
  constructor(props) {
    super();
    this.state = {
      list: []
    }
  }
  componentDidMount() {
    loadUsers()
      .then(data=> 
        this.setState({list: data.userList})
      )
  }
  render() {
    return (
      <List list={this.state.list} />
    )
  }
  /* other bussiness logics */
}

實際狀況中,以上代碼每每還會和其它一些業務函數混雜在一塊兒——咱們建立了一個業務反作用混雜的、有狀態的組件。

若是再來一個書單列表呢?再寫一個BookList而後把loadUsers改爲loadBooks ?
不只代碼重複,大量有狀態和反作用的組件,也使得應用更加難以測試。

也許你會考慮使用Flux。它確實能讓你的代碼更清晰,可是在有些場景下使用Flux就像大炮打蚊子。好比一個異步的下拉選擇框,若是要考慮複用的話,傳統的Flux/Reflux幾乎沒法優雅的處理,Redux稍好一些,但仍然很難作優雅。關於flux/redux的缺點不深刻,有興趣的能夠參考Cycle.js做者的文章

回到問題的本源:其實咱們只想要一個能複用的異步下拉列表而已啊!

高階函數試試?

import React, { Component } from 'react';

const DEFAULT_OPTIONS = {
  mapStateToProps: undefined,
  mapLoadingToProps: loading => ({ loading }),
  mapDataToProps: data => ({ data }),
  mapErrorToProps: error => ({ error }),
};
export function connectPromise(options) {
  return (Comp) => {
    const finalOptions = {
      ...DEFAULT_OPTIONS,
      ...options,
    };
    const {
      promiseLoader,
      mapLoadingToProps,
      mapStateToProps,
      mapDataToProps,
      mapErrorToProps,
    } = finalOptions;

    class AsyncComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          loading: true,
          data: undefined,
          error: undefined,
        };
      }
      componentDidMount() {
        promiseLoader(this.props)
          .then(
            data => this.setState({ data, loading: false }),
            error => this.setState({ error, loading: false }),
          );
      }
      render() {
        const { data, error, loading } = this.state;

        const dataProps = data ? mapDataToProps(data) : undefined;
        const errorProps = error ? mapErrorToProps(error) : undefined;

        return (
          <Comp
            {...mapLoadingToProps(loading)}
            {...dataProps}
            {...errorProps}
            {...this.props}
          />
        );
      }
    }

    return AsyncComponent;
  };
}


const UserList = connectPromise({
    promiseLoader: loadUsers,
    mapDataToProps: result=> ({list: result.userList})
})(List); //List can be a pure component

const BookList = connectPromise({
    promiseLoader: loadBooks,
    mapDataToProps: result=> ({list: result.bookList})
})(List);

不只大大減小了重複代碼,還把散落各處的異步邏輯裝進了能夠單獨管理和測試的籠子,在業務場景中,只須要純組件 + 配置 就能實現相同的功能——而不管是純組件仍是配置,都是對單元測試友好的,至少比異步組件友好多了。

使用curry & compose

高階組件的另外一個亮點,就是對函數式編程的友好。你可能已經注意到,目前我寫的全部高階函數,都是形如:

config => {
    return Component=> {
        return HighOrderCompoent
    }
}

表示爲config=> Component=> Component

寫成嵌套的函數是爲了手動curry化,而參數的順序(爲何不是Component=> config=> Component),則是爲了組合方便。關於curry與compose的使用,能夠移步個人另外一篇blog

舉個栗子,前面講了適配器和異步,咱們能夠很快就組合出二者的結合體:使用NewList的異步用戶列表

UserList = compose(
  connectPromise({
    promiseLoader: loadUsers,
    mapResultToProps: result=> ({list: result.userList})
  }),
  mapProps(mapPropsForNewList)
)(NewList);

總結

在團隊內部分享裏,個人總結是三個詞 Easy, Light-weight & Composable.

其實高階組件並非什麼新東西,本質上就是Decorator模式在React的一種實現,但在至關一段時間內,這個優秀的模式都被人忽略。在我看來,大部分使用mixinclass extends的地方,高階組件都是更好的方案——畢竟組合優於繼承,而mixin——我的以爲沒資格參與討論。

使用高階組件還有兩個好處:

  1. 適用範圍廣,它不須要es6或者其它須要編譯的特性,有函數的地方,就有HOC。

  2. Debug友好,它可以被React組件樹顯示,因此能夠很清楚地知道有多少層,每層作了什麼。相比之下不管是mixin仍是繼承,都顯得很是隱晦。

值得慶幸的是,社區也明顯注意到了高階組件的價值,不管是你們很是熟悉的react-reduxconnect函數,仍是redux-form,高階組件的應用開始隨處可見。

下次當你想寫mixinclass extends的時候,不妨也考慮下高階組件。

相關文章
相關標籤/搜索