[譯]React高級指引7:高階組件

原文連接:reactjs.org/docs/higher…html

引言

在React中高階組件(HOC)是用於複用組件邏輯的一種高階技巧。高階組件自身並非React API的一部分。它是基於React組合特性而設計的一種模式。react

具體來講,高階組件就是一個接收組件做爲參數並返回一個新組件的函數git

const EnhancedComponent = higherOrderComponent(WrappedComponent);
複製代碼

組件將props轉化成UI,而高階組件則將組件轉化成另外一個組件。github

高階組件在第三方庫中是十分常見的,好比Redux的connect,和Relay的createFragmentContainer算法

在本節中咱們將講述爲何高階組件是有用的,如何來構建咱們本身的高階組件。redux

使用高階組件解決橫切關注點問題

注意: 咱們以前推薦使用mixins來解決橫切關注點問題。可是如今咱們已經瞭解到使用mixins會帶來更多的問題。閱讀更多瞭解爲何咱們要拋棄mixins以及如何遷移已經編寫好的組件。api

組件是React代碼複用的基本單位。可是在實踐過程當中中你會發現傳統的組件沒法直接適應某些模式。數組

好比,你如今有一個CommentList組件,它接收一個外部數據源來渲染評論:bash

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource"是某些全局數據源
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // 註冊change事件監聽器
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除監聽器
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 當數據源改變時更新state
    this.setState({
      comments: DataSource.getComments()
    });
  }

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

以後,你編寫了一個訂閱單個博客帖子的組件,這個組件也是用了與上面相似的模式:app

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} />;
  }
}
複製代碼

CommentListBlogPost是不一樣的——它們在DataSource上調用了不一樣的方法而且渲染結果不一樣。可是它們大部分的實現細節是相同的:

  • 在組件掛載完成後,爲DataSource添加change監聽器;
  • 在監聽器內部,在數據源更改時調用setState;
  • 在組件卸載時移除監聽器。

你能夠想象,在一個大型應用中,這種訂閱DataSource和調用setState的行爲是一直存在的。咱們想要一個抽象方法,可以只在一個地方編寫邏輯,而後把這段邏輯共享給須要的組件。這就是高階組件擅長的地方。

咱們如今來建立一個函數,這個函數可以建立CommentListBlogPost,訂閱DataSource。這個函數將會接收一個子組件做爲它的參數之一,這個子組件將會接收訂閱數據做爲props。如今讓咱們稱這個函數爲withSubscription

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

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

第一個參數是被包裹的組件。第二個參數根據咱們給定的DataSource和prop返回咱們須要的數據。

CommentListWithSubscriptionBlogPostWithSubscription被渲染時CommentListBlogPost將會接收從當前DataSource中計算獲得的數據做爲data prop:

// 這個函數接收一個組件做爲參數...
function withSubscription(WrappedComponent, selectData) {
  // ...並返回另外一個組件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... 負責訂閱的相關操做...
      DataSource.addChangeListener(this.handleChange);
    }

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

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

    render() {
      //... 用最新的數據渲染包裹的組件
      //注意咱們會傳遞其餘數據
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
複製代碼

注意高階組件不會修改輸入的組件,也不會用繼承來複制它的行爲。相反的,高階組件將原始組件包裹在容器組件中。一個高階組件應該是純函數,沒有任何反作用。

被包裹的組件從容器組件中獲取了全部須要的props,同時也接收一個用於渲染的prop data。高階組件不關心data是怎麼被使用的,而被包裹的組件無論數據從哪來的。

這是由於withSubscription是一個正常的函數,你能夠任意添加你想要的參數。好比你想要data prop的名字是可配置的。以進一步將高階組件和被包裹的組件分離。或者你能夠接受一個配置shouldComponentUpdate的參數,或者可以配置數據源的參數。因爲高階組件能夠控制如何定義組件,因此這些都是可行的。

就像組件同樣,withSubscription與被包裹組件的聯繫是徹底基於props的。這種關係使得更換高階組件十分簡單,只要可以提供一樣的props給被包裹組件就能夠了。好比這在你更換數據獲取的第三方庫時很是有用。

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

不要試圖在高階組件中修改組件的原型(prototype)或用其餘任何方式修改它。

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps = function(nextProps) {
    console.log('Current props: ', this.props);
    console.log('Next props: ', nextProps);
  };
  //返回原始數組,暗示它已經被修改
  return InputComponent;
}

// EnhancedComponent將會在接收到prop時在控制檯打印結果
const EnhancedComponent = logProps(InputComponent);
複製代碼

這裏有幾個問題。一是輸入組件沒法像高階組件加強以前使用了。更重要的是,若是你將EnhancedComponent包裹在另外一個能夠修改EnhancedComponent的高階組件中,那麼第一個高階組件的功能將被覆蓋!同時這個高階組件沒法應用於沒有生命週期的函數組件。

修改輸入組件的高階組件是一種糟糕的抽象方式——調用者必須知道它們是如何實現的以免與其餘高階組件發生衝突。

相比於修改,高階組件應該使用組合,將輸入組件包裹在一個容器組件中:

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      // 將輸入組件包裹在容器組件中,而不是試圖修改它
      return <WrappedComponent {...this.props} />;
    }
  }
}
複製代碼

上面的高階組件的功能和修改輸入組件的高階組件功能相同,可是避免了潛在的衝突問題。它可以很好地運用於class組件和函數組件。並且因爲它是一個純函數,它能夠和其餘高階組件組合使用,甚至和它自身組合使用。

也許你已經發現了高階組件和容器組件之間的相同之處。容器組件是分離高層關注和底層關注的策略之一。容器組件使用訂閱和state管理事務,而且傳遞props給那些須要數據的組件。高階組件使用容器組件做爲實現自身的一部分。能夠將高階組件看成是參數化的容器組件。

約定:將不想管的props傳遞給被包裹的組件

高階組件給組件添加了一些特性。它們自身不能大幅度修改約定。一般咱們但願從高階組件返回的組件與輸入組件有類似的交互界面。

高階組件應該透傳與自身無關的props。大部分高階組件包含了相似於下面的render方法:

render() {
  // 過濾出與高階組件有關的額外props而且不透傳它們。
  const { extraProp, ...passThroughProps } = this.props;

  // 將props注入被包裹組件。這些props一般是
  // state值或者實例函數
  const injectedProp = someStateOrInstanceMethod;

  // 將props傳遞給被包裹組件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}
複製代碼

這個約定確保了高階組件是靈活可複用的。

約定:最大化可組合性

並非全部的高階組件都看起來是同樣的。有時候高階組件只接受一個參數:被包裹組件:

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

一般高階組件都會接收額外的參數。在下面的關於Relay的例子中,額外的參數config對象被用來聲明組件的數據依賴:

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

最多見高階組件簽名以下:

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

這是什麼鬼玩意??可是你把它分開,就能夠更清晰地瞭解到它的機制。

// connect是一個函數,它返回了另外一個函數
const enhance = connect(commentListSelector, commentListActions);
// 返回的函數是一個高階組件,它返回了一個
//與Redux store相關聯的組件
const ConnectedComment = enhance(CommentList);
複製代碼

換句話說,connect是一個高階函數,它返回了一個高階組件!

這種形式可能看起來讓人困惑或沒必要要,可是它有一個很是有用的屬性。就像connect函數返回的單一參數高階組件同樣,它有一個簽名Component => Component。輸入類型與輸出類型相同的函數是很是容易組合的。

// 不要這樣...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// ... 你能夠編寫組合工具函數
// compose(f, g, h) 與 (...args) => f(g(h(...args)))相同
const enhance = compose(
  //它們都是單一參數高階組件
  withRouter,
  connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
複製代碼

(一樣的屬性也容許connect和其餘高階組件承擔裝飾者的角色,裝飾者是JavaScript一項實驗性的提案。)

許多第三方庫都提供了compose工具函數,好比lodash(lodash.flowRight),Redux,Ramda

約定:包裹顯示名稱一遍輕鬆調試

由高階組件建立的容器組件會在React Developer Tools中像其餘組件同樣顯示。爲了能更好地調試,選擇一個展現名稱來顯示它是高階組件建立的組件。

最經常使用的方法是包裹被包裹組件的展現名稱。因此若是你的高階組件的名字是withSubscription,被包裹組件的展現名稱是CommentList,那麼就使用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';
}
複製代碼

注意事項

高階組件有一些注意事項,對於剛接觸React的人來講可能不容易法相。

不要在render方法中使用高階組件

React的diff算法使用組件的身份標誌來決定是否更新子組件樹仍是丟棄並從新掛載新的子組件樹。若是render方法返回的組件與上一次渲染的組件一致(===),React將會根據diff算法在子組件樹和新的子組件樹進行遞歸更新。若是它們不是相同的,那麼子組件樹將會被徹底卸載。

正常來講,你不須要考慮這個問題。可是這對高階組件來講很重要,由於者意味着你不能在組件的render方法中使用高階組件來返回組件:

render() {
  //每一次更新時都會建立一個新的EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  //這樣作會致使整個子組件樹在每次渲染時都被卸載而後從新掛載!
  return <EnhancedComponent />;
}
複製代碼

這不只僅是性能問題——從新掛載組件將會致使它的state狀態和全部的子元素的丟失。

相反,若是在組件以外調用高階組件,那麼組件只會建立一次。在這以後,組件的身份標誌將會在整個渲染過程當中保持一致。這纔是咱們想要的。

儘管不多遇到,但有時候你仍是會須要動態地使用高階組件,你能夠在組件的生命週期方法或者構造函數中使用高階組件。

務必複製靜態方法

有時候在React組件中定義一個靜態方法是十分有用的。好比,Relay容器暴露了一個getFragment靜態方法來促進對GraphQL片斷的組合。

當你將高階組件應用於組件時,原始組件將被包裹在容器組件中。但這意味着新的組件將不持有任何原始組件的靜態方法。

// 定義一個靜態方法
WrappedComponent.staticMethod = function() {/*...*/}
// 如今調用一個高階組件
const EnhancedComponent = enhance(WrappedComponent);

// 新的加強組件是沒有靜態方法的
typeof EnhancedComponent.staticMethod === 'undefined' // true
複製代碼

爲了解決這個問題,你能夠在返回這個容器組件以前將這些靜態方法複製給它:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  //必需要知道須要複製哪些方法
  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;
}
複製代碼

另外一種解決方法是再另外導出這個靜態方法:

// 不要這麼作...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...單獨地將方法導出...
export { someFunction };

// ...在消費模塊中,將這二者都引入
import MyComponent, { someFunction } from './MyComponent.js';
複製代碼

Refs不會被透傳

儘管高階組件的規則是將全部的props都透傳給被包裹組件,但對refs例外。這是由於refs不是真正的prop,就像key同樣,它被React特殊對待。若是你爲一個高階組件產生的組件添加了ref,那麼這個ref引用的是最外層的容器組件而不是被包裹的組件。

解決方案是使用React.forwardRef API(在React16.3中引進),在Refs轉發中瞭解更多。

相關文章
相關標籤/搜索