[譯]React高級話題之高階組件

前言

本文爲意譯,翻譯過程當中摻雜本人的理解,若有誤導,請放棄繼續閱讀。javascript

原文地址:Higher-Order Componentshtml

正文

高階組件(後文中均以HOCs來指代)是React生態裏面的一種用來複用組件邏輯的高級技術。HOCs自己並非React API的一部分,而是一種從React的可組合性中產生的模式。java

具體來講,HOCs其實就是一個函數。只不過這個函數跟編程語言中普通的函數不一樣的是,它接受一個React組件做爲輸入,返回了一個新的React組件。react

const EnhancedComponent = higherOrderComponent(WrapperComponent)
複製代碼

咱們從轉化的角度能夠這麼說:「若是說,React組件是將props轉化爲UI,那麼HOCs就是將一箇舊的組件轉化爲一個新的組件(通常狀況下,是做了加強)」。HOCs在第三方類庫中很常見,好比:Redux的connect方法,Relay的createFragmentContainergit

在這個文檔裏面,我麼將會討論爲何HOCs這麼有用和咱們該怎樣寫本身的高階組件。github

使用HOCs來完成關注點分離

注意:咱們以前一直在推薦使用mixins來完成關注點分離。可是後面咱們發現了mixins所帶來的問題遠大於它存在所帶來的價值,咱們就放棄使用它了。查閱這裏,看看咱們爲何放棄了mixins,而且看看你能夠如何升級你的現有組件。算法

在React中,組件是代碼複用的基本單元。然而你會發現,一些模式並不能簡單地適用於傳統意義上的組件。編程

舉個例子來講,假設你有一個叫CommentList的組件。這個組件訂閱了一個外部的數據源,最終將渲染出組件列表。redux

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div> {this.state.comments.map((comment) => ( <Comment comment={comment} key={comment.id} /> ))} </div> ); } } 複製代碼

然後,你又以相同的模式去寫了一個用於訂閱一篇博客文章的組件。以下:api

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />; } } 複製代碼

嚴格上來講,CommentList和BlogPost是不徹底相同的。它們分別在DataSource上調用不一樣的方法,渲染了不一樣的UI。可是,除了這些點以外,它們大部分是相同的:

  • 在掛載以後,都往DataSource裏面註冊了一個change listener。
  • 在change listener裏面,當數據源發生改變時都調用了setState。
  • 在卸載以前,都要移除change listener。

你能夠想象,在一個大型的項目中,這種模式的代碼(訂閱一個DataSource,而後在數據發生變化的時候,調用setState來更新UI)會處處出現。咱們須要將這種邏輯抽取出來,定義在單獨的地方,而後跨組件去共用它。而,這偏偏是HOCs要作的事情。

咱們能夠寫一個函數用於建立像CommentList和BlogPost那樣訂閱了DataSource的組件。這個函數將會接收子組件做爲它的一個參數。而後這個子組件會接收訂閱的數據做爲它的prop。咱們姑且稱這個函數爲withSubscription。

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);
複製代碼

第一個參數是被包裹的組件(wrapped component),第二個參數是一個函數,負責經過咱們傳遞進去的DataSource和props來獲取並返回咱們須要的數據。

當CommentListWithSubscription和BlogPostWithSubscription被渲染以後,組件CommentList和BlogPost的data屬性將會獲得從訂閱源DataSource訂閱回來的數據。

// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />; } }; } 複製代碼

注意,HOCs並無篡改咱們傳遞進入的組件,也沒有繼承它而後複製它的行爲。HOCs只是單純地將咱們傳遞進入的組件包裹在一個容器組件(container component)。一個高階組件是純函數,不能包含任何的反作用。

就這麼多。wrapped component接受從container component傳遞下來的全部props,與此同時,還多了一個用於渲染最終UI的,新的prop:data。HOC它不關注你怎麼使用數據,爲何要這樣使用。而wrapped component也不關心數據是從哪裏來的。

由於withSubscription只是一個普通函數,你能夠定義任意多的參數。舉個例子,你想讓data 屬性變得更加的可配置,以便將HOC和wrapped component做進一步的解耦。又或者你能夠增長一個參數來定義shouldComponentUpdate的實現。這些都是能夠作到的,由於HOC只是一個純函數而已,它對組件的定義擁有百分百的話語權。

正如React組件同樣,高階組件withSubscription跟wrapped component的惟一關聯點只有props。這樣的清晰的關注點分離,使得wrapped component與其餘HOC的結合易如反掌。前提是,另一個HOC也提供相同的props給wrapped component。就拿上面的例子來講,若是你切換data-fetching類庫(DataSource),這將會是很簡單的。

戒律

1.不要修改原始組件,使用組合。

在HOC的內部,要抵制修改原始組件的prototype的這種誘惑(畢竟這種誘惑是觸手可及的)。

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  // The fact that we're returning the original input is a hint that it has
  // been mutated.
  return InputComponent;
}

// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
複製代碼

這樣作有如下的幾個壞處:

  • 原始組件不能脫離該高階組件去複用了。
  • 假如你把EnhancedComponent應用到另一個HOC,剛好這個HOC在原型鏈上也作了一樣的修改。那麼,你第一個HOC的功能就被覆蓋掉了。
  • 上述例子中的寫法不能應用於function component。由於function component沒有生命週期函數。
  • 形成抽象封裝上的漏洞。一旦你這麼作了,那麼使用者爲了不衝突,他必須知道你到底在上一個HOC對wrapped component 作了什麼樣的修改,以避免他也做出一樣的修改而致使衝突。

相對於修改,HOCs應該使用組合來實現。也就是說,把傳遞進來的組件包裹到container component當中。

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

上面這個組合版的HOC跟修改版的擁有相同的功能。相比修改版的HOC,它很好地避免了一些潛在的衝突。同時,它也能很好地跟function component和class component組合使用。最後,它也能很方便地跟其餘HOC組合使用,或者甚至跟它本身。

container component是對高層級關注點與低層級關注點進行職責分離策略的一部分。在這個策略裏面,container component負責管理數據訂閱和保存state,而且將全部的數據衍生爲props傳遞給它的子組件,而後子組件負責渲染UI。HOCs把container模式做爲了它實現的一部分。你能夠理解爲HOC是參數化的container component定義。

2.不要在React組件的render方法中使用HOCs

React的diff算法(也稱之爲reconciliation)是根據component的惟一標識(component identity )來決定這個組件是否應該從已經存在的子組件樹中更新仍是完全棄用它,掛載一個新的組件。若是一個component的render函數的返回值全等於(===)另一個組件render函數的返回值,那麼React就認爲他們是同一個組件,而後遞歸地更新這個組件的子組件樹。不然的話,就徹底卸載以前的那個組件。

一般來講,你不須要考慮這些東西。可是,在使用HOCs的時候,你須要作這樣的考慮。由於你不能在一個組件的render方法裏面使用HOC。以下:

render() {
  // A new version of EnhancedComponent is created on every render
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // That causes the entire subtree to unmount/remount each time!
  return <EnhancedComponent />; } 複製代碼

像上面這樣的作法會致使性能上的問題。什麼問題呢?那就是從新掛載一個組件會致使沒法利用現有的組件state和子組件樹。 而咱們想要的偏偏相反。咱們想要的加強後的組件的標識在屢次render調用過程當中都是一致的。要想達成這種效果,咱們須要在組件定義的外部去調用HOC來僅僅建立一次這個加強組件。

在極少數的狀況下,你可能想動態地使用HOC,你能夠在組件的非render生命週期函數或者constructor裏面這麼作。

約定俗成

1. 將(HOC)非相關的props傳遞給Wrapped component

HOCs本質就是給組件增長新的特性。他們不該該去大幅度地修改它與wrapped component的契約之所在-props。咱們期待從HOC返回的新的組件與wrapped component擁有相同的接口(指的也是props)。

HOCs應該將它不關注的props原樣地傳遞下去(給加強後的新組件)。大部分的HOCs都會包含一個render方法,這個render方法看起來是這樣的:

render() {
  // Filter out extra props that are specific to this HOC and shouldn't be
  // passed through
  const { extraProp, ...passThroughProps } = this.props;

  // Inject props into the wrapped component. These are usually state values or
  // instance methods.
  const injectedProp = someStateOrInstanceMethod;

  // Pass props to wrapped component
  return (
    <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); } 複製代碼

咱們這麼作的目的是讓HOCs能作到儘量的可擴展和可複用。

2. 可組合性最大化

並非全部的HOCs看起來都是同樣的。有一些HOC僅僅接收一個參數-wrapped component。

const NavbarWithRouter = withRouter(Navbar);
複製代碼

通常來講,HOCs會接收其他的一些參數。好比說Relay的createContainer方法,它的第二個參數就是一個配置型的參數,用於指明組件的數據依賴。

const CommentWithRelay = Relay.createContainer(Comment, config);
複製代碼

HOCs最多見的函數簽名是這樣的:

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
複製代碼

什麼鬼?若是你把這行代碼拆開來看,你就會知道這究竟是怎麼回事。

// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is a HOC, which returns a component that is connected
// to the Redux store
const ConnectedComment = enhance(CommentList);
複製代碼

換句話說,connect就是一個返回高階組件的高階函數。(注意,原文這裏用的是higher-orderfunction 和 higher-order component)!

這種寫法可能看起來是讓人疑惑或者是多餘的,實際上,它是有用的。從函數式編程的角度來說,那種參數類型和返回類型一致的單參數函數是很容易組合的。而connect之因此要這麼實現,也是基於這種考慮。也就是說,相比這種簽名的函數(arg1,component)=> component,component => component 類型的函數更容易跟同類型的函數組合使用。具體的示例以下:

// Instead of doing this...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... you can use a function composition utility
// compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
  // These are both single-argument HOCs
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
複製代碼

像connect的這種類型寫法的函數能夠被用做ES7提案特性之一的裝飾器(decorators)。

像compose這種工具函數不少第三方的類庫都會提供,好比說lodash(loadash.flowRight),ReduxRamda

3. 給HOC追加displayName屬性

那個被HOCs建立的container component在React Developer Tools中長得跟普通的組件是同樣的。爲了更方便調試,咱們要選擇一個display name 給它。

最多見的作法是給container component的靜態屬性displayName直接賦值。假如你的高階組件叫withSubscription,wrapped component叫CommentList,那麼container component的displayName的值就是WithSubscription(CommentList)。以下:

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
複製代碼

注意點

1. 記得要把Wrapped component的靜態方法複製到加強後的組件中去

有時候,在React component上定義一個靜態方法仍是挺有用的。好比說,Relay container就暴露了一個叫getFragment靜態方法來方便與GraphQL fragments的組合。

當你把一個組件傳遞進HOC,也僅僅意味着你把它包裹在container component當中而已。由於加強後的組件並無「繼承」原組件的全部靜態方法。

// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);

// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼

爲了解決這個問題,你能夠在HOC的內部先將原組件的靜態方法一一複製了,再返回出去。

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // Must know exactly which method(s) to copy :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}
複製代碼

然而,你這麼作很差的一點是,你必須知道你須要複製的靜態方法有哪些。你可使用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;
}
複製代碼

另一個能夠考慮得解決方案是,在定義原組件的時候,把組件和這個組件的靜態方法分開導出。而後在使用的時候,分開來導入。

// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...export the method separately...
export { someFunction };

// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
複製代碼

2. 記得將ref屬性傳遞下去

雖說,將全部的prop都原樣傳遞下去是實現HOC的一個慣例,可是這種傳遞對ref這個屬性不起做用。那是由於,嚴格意義上說,ref並非一個真正的prop,key也不是。它倆都是專用於React的內部實現的。若是你在一個由HOC建立並返回的組件(也就是說加強後的組件)上增長了ref屬性,那麼這個ref屬性指向的將會是HOC內部container component最外層那個組件的實例,而不是咱們期待的wrapped component。

針對這個問題的解決方案是使用React.forwardRef這個API(在React的16.3版本引入的)。關於React.forwardRef,你能夠查閱更多

相關文章
相關標籤/搜索