React文檔(二十四)高階組件

高階組件(HOC)是React裏的高級技術爲了應對重用組件的邏輯。HOCs本質上不是React API的一部分。它是從React的組合性質中顯露出來的模式。html

具體來講,一個高階組件就是一個獲取一個組件並返回一個組件的函數react

const EnhancedComponent = higherOrderComponent(WrappedComponent);

然而一個組件將props轉變爲UI,一個高階組件將一個組件轉變爲另一個組件。git

HOCs在第三方React庫裏也是有的,就像Redux裏的connect和Relay裏的createContainer。github

在這篇文檔裏,咱們將討論爲何高階組件有用處,還有怎樣來寫你本身的高階組件。算法

爲橫切關注點使用HOCs數組

注意:app

咱們之前建議使用mixins來處理橫切關注點的問題。但如今意識到mixins會形成不少問題。讀取更多信息關於爲何咱們移除mixins以及怎樣過渡已存在的組件。ide

在React裏組件是主要的重用代碼單元。然而,你會發現一些模式並不直接適合傳統的組件。函數

舉個例子,假設你有一個CommentList組件它訂閱了一個外部數據源來渲染一組評論:工具

class CommentList extends React.Component {
  constructor() {
    super();
    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>
    );
  }
}
而後,你寫一個組件來訂閱一個單個博客帖子,遵循一個相似的模式:
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不徹底相同。它們在DateSource上調用不一樣的方法,而後它們渲染出不一樣的輸出。可是它們大多數實現是同樣的:

  • 在初始裝載的時候,給DataSource添加一個監聽改變的監聽器
  • 在監聽器內部,當數據源變化的時候調用setState
  • 銷燬的時候,移除監聽器

你能夠想象在一個大型app裏,訂閱到DataSource而且調用setState這個一樣的模式會一遍又一遍的重複發生。咱們所以就想將這重複的過程抽象化,讓咱們在一個單獨的地方定義這段邏輯而且在多個組件中均可以使用這段邏輯。這就是高階組件所擅長的。

咱們能夠寫一個建立組件的函數,就像CommentList和BlogPost,它們訂閱到DataSource。這個函數會接受一個參數做爲子組件,這個子組件接收訂閱的數據做爲prop。讓咱們調用這個函數eithSubscription:

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

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
});

withSubscription的第一個參數是被包裹起來的組件。第二個參數檢索咱們喜歡的數據,給出一個DateSource和當前的props。

但CommentListWithSubscription和BlogPostWithSubscription被渲染了,CommenList和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} />;
    }
  };
}

注意一個HOC不會修改傳入的參數組件,也不會使用繼承來複制它的行爲。不如說,一個HOC經過將原始組件包裹到一個容器組件裏來混合。一個HOC是一個沒有反作用的純函數。

就是這樣!被包裹的組件接受全部容器組件的props,還有和一個新的屬性,data一塊兒渲染它的輸出。HOC不會關心數據怎樣或者爲何使用,被包裹的組件也不會關心數據是從哪裏來的。

由於withSubscription是一個普通的函數,你能夠添加或多或少的參數根據狀況。舉個例子,你也許想要使data屬性的名字是可配置的,這樣就能夠進一步從包裹的組件隔離HOC。或者你能夠接受一個參數來配置shouldComponentUpdate,或者一個參數來配置數據源。這些均可以由於HOC擁有全部權利去定義組件。

相似於組件,withSubscription和被包裹的組件之間的不一樣是徹底基於props的。這就能夠很容易地去交換一個HOC和另外一個,只要他們提供給被包裹組件的props是同樣的。舉個例子,這樣若是你改變提取數據的庫就會頗有用。

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

在HOC裏要打消修改組件原型的想法。

function logProps(InputComponent) {
  InputComponent.prototype.componentWillReceiveProps(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那樣也還會改變componentWillReceiveProps,先前的HOC的功能會被重寫!這個HOC也不能憑藉函數式組件來工做,也沒有生命週期方法。

改變HOC是一個脆弱的抽象,用戶必須知道他們是怎樣實現的爲了不和其餘HOC發生衝突。

不用去修改,而應該使用組合,經過將輸入的組件包裹到一個容器組件裏:

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功能同樣然而避免了潛在的衝突。它和類組件還有函數組件都工做地很好。由於它是純函數,因此它能夠由其餘HOC組成,甚至用它本身也能夠。

你也許注意到了HOC和容器組件這個模式的類似之處。容器組件是將高階和低階關注點的功能分離的策略的一部分。

容器管理相似訂閱和state的東西,而且傳遞props給組件而後處理相似渲染UI的事。HOC將容器做爲實現的一部分。你能夠把HOC看作參數化的容器組件的定義。

約定:給被包裹的元素傳遞不相關的props

HOC給一個組件添加特性。它們不該該完全改變它的約定。那就是HOC返回的組件擁有一個和被包裹的組件相似的界面。

HOC應該傳遞與肯定的關注點不相關的props。多數HOC包含一個渲染方法看起來就像這樣:

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}
    />
  );
}

這個約定確保HOC儘量地靈活和可重用。

約定:最大化可組合性

不是全部的高階組件看起來都同樣。有時候它們只接收一個參數,被包裹的組件:

const NavbarWithRouter = withRouter(Navbar);

一般HOC會接收額外的參數。在Relay的例子裏,一個config對象被用於指定組件的數據依賴:

const CommentWithRelay = Relay.createContainer(Comment, config);

HOC最多見的簽名是這樣的:

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(Comment);

什麼?!若是你將步驟分離,就能夠很容易看出發生了什麼。

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

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

這種形式也許看起來讓人迷惑或者不是很重要,可是它有一個有用的屬性。單參數的HOC例如connect函數返回的那一個擁有這樣的鮮明特徵Component => Component(組件 => 組件)。輸出類型和輸入類型相同的函數就很容易組合到一塊兒。

// Instead of doing this...
const EnhancedComponent = connect(commentSelector)(withRouter(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
  connect(commentSelector),
  withRouter
)
const EnhancedComponent = enhance(WrappedComponent)

(這個同樣的屬性一樣容許connect和其餘加強器HOC被做爲裝飾來使用,這是一個實驗性的js提案)

compose這個實用函數是不少第三方庫提供的,包括lodash(lodash.flowRight),Redux,Ramda。

約定:包裹顯示名字爲了使調試更加簡單

就像其餘組件同樣,HOC建立的容器組件也會顯示在React Developer Tools工具裏。想讓調試更加簡單,選擇一個顯示名字來通信這是一個HOC的結果。

最廣泛的技術是包裹被包裹函數的顯示名字。因此若是你的高階函數的名字是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函數裏使用HOC

React的diffing算法(被稱爲一致)使用組件的一致來決定是否應該更新已存在的子樹或者放棄更新或者從新建立一個新的。若是render返回的組件和上一次render的組件同樣(===),React就會經過比較二者的不一樣來遞歸地更新子樹。若是它們不同,那麼先前的子樹就徹底被銷燬。
一般,你不須要思考這些。可是這對HOC來講很重要由於這意味着你不能在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和它全部的子元素丟失。

在組件定義的外部來使用HOC使得結果組件只被建立一次。以後,它的身份在渲染過程當中會一直保持不變。總之,這就是你常常想要的結果。

在這些罕見的狀況裏你須要動態的運用HOC,你也能夠在組建的生命週期函數裏或者構造函數裏使用。

靜態方法必須被複制

有些時候在React組件裏定義一個靜態方法是頗有用的。舉個例子,Relay容器暴露了一個靜態方法getFragment爲了促進GraphQL片斷的組成。

當你爲一個組件運用了HOC,雖然原始組件被一個容器組件所包裹。這意味着新的組件沒有任何原始組件的靜態方法。

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

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

爲了解決這個問題,你能夠在返回它以前在容器組件之上覆制那些方法。

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';
相關文章
相關標籤/搜索