React 組件設計和分解思考

以前分享過幾篇關於React技術棧的文章:html

今天再來同你們討論 React 組件設計的一個有趣話題:分解 React 組件的幾種進階方法。編程

React 組件魔力無窮,同時靈活性超強。咱們能夠在組件的設計上,玩轉出不少花樣。可是保證組件的Single responsibility principle: 單一原則很是重要,它可使得咱們的組件更簡單、更方便維護,更重要的是使得組件更加具備複用性。redux

可是,如何對一個功能複雜且臃腫的 React 組件進行分解,也許並非一件簡單的事情。本文由淺入深,介紹三個分解 React 組件的方法。架構

切割 render() 方法

這是一個最容易想到的方法:當一個組件渲染了不少元素時,就須要嘗試分離這些元素的渲染邏輯。最迅速的方式就是切割 render() 方法爲多個 sub-render 方法。

看下面的例子會更加直觀:

class Panel extends React.Component {
  renderHeading() {
    // ...
  }

  renderBody() {
    // ...
  }

  render() {
    return (
      <div>
        {this.renderHeading()}
        {this.renderBody()}
      </div>
    );
  }

細心的讀者很快就能發現,其實這並無分解組件自己,該 Panel 組件仍然保持有原先的 state, props, 以及 class methods。

如何真正地作到減小複雜度呢?咱們須要建立一些子組件。此時,採用最新版 React 支持並推薦的函數式組件/無狀態組件必定會是一個很好的嘗試:

const PanelHeader = (props) => (
  // ...
);

const PanelBody = (props) => (
  // ...
);

class Panel extends React.Component {
  render() {
    return (
      <div>
        // Nice and explicit about which props are used
        <PanelHeader title={this.props.title}/>
        <PanelBody content={this.props.content}/>
      </div>
    );
   }
 }

同以前的方式相比,這個微妙的改進是革命性的。咱們新建了兩個單元組件:PanelHeader 和 PanelBody。這樣帶來了測試的便利,咱們能夠直接分離測試不一樣的組件。同時,藉助於 React 新的算法引擎 React Fiber,兩個單元組件在渲染的效率上,樂觀地預計會有較大幅度的提高。

模版化組件

回到問題的起點,爲何一個組件會變的臃腫而複雜呢?其一是渲染元素較多且嵌套,另外就是組件內部變化較多,或者存在多種 configurations 的狀況。

此時,咱們即可以將組件改造爲模版:父組件相似一個模版,只專一於各類 configurations。

仍是要舉例來講,這樣理解起來更加清晰。

好比咱們有一個 Comment 組件,這個組件存在多種行爲或事件。同時組件所展示的信息根據用戶的身份不一樣而有所變化:用戶是不是此 comment 的做者,此 comment 是否被正確保存,各類權限不一樣等等都會引發這個組件的不一樣展現行爲。這時候,與其把全部的邏輯混淆在一塊兒,也許更好的作法是利用 React 能夠傳遞 React element 的特性,咱們將 React element 進行組件間傳遞,這樣就更加像一個模版:

class CommentTemplate extends React.Component {
  static propTypes = {
    // Declare slots as type node
    metadata: PropTypes.node,
    actions: PropTypes.node,
  };
  
  render() {
    return (
      <div>
        <CommentHeading>
          <Avatar user={...}/>
          
          // Slot for metadata
          <span>{this.props.metadata}</span>
          
        </CommentHeading>
    
        <CommentBody/>
        
        <CommentFooter>
          <Timestamp time={...}/>
          
          // Slot for actions
          <span>{this.props.actions}</span>
          
        </CommentFooter>
      </div>
      ...

此時,咱們真正的 Comment 組件組織爲:

class Comment extends React.Component {
  render() {
    const metadata = this.props.publishTime ?
      <PublishTime time={this.props.publishTime} /> :
      <span>Saving...</span>;
    
    const actions = [];
    if (this.props.isSignedIn) {
      actions.push(<LikeAction />);
      actions.push(<ReplyAction />);
    }
    if (this.props.isAuthor) {
      actions.push(<DeleteAction />);
    }
    
    return <CommentTemplate metadata={metadata} actions={actions} />;
  }

metadata 和 actions 其實就是在特定狀況下須要渲染的 React element。

好比,若是 this.props.publishTime 存在,metadata 就是 <PublishTime time={this.props.publishTime} />;反正則爲 <span>Saving...</span>。

若是用戶已經登錄,則須要渲染(即actions值爲) <LikeAction /> 和 <ReplyAction />,若是是做者自己,須要渲染的內容就要加入 <DeleteAction />。

高階組件

在實際開發當中,組件常常會被其餘需求所污染。

好比,咱們想統計頁面中全部連接的點擊信息。在連接點擊時,發送統計請求,同時包含此頁面 document 的 id 值。常見的作法是在 Document 組件的生命週期函數 componentDidMount 和 componentWillUnmount 增長代碼邏輯:

class Document extends React.Component {
  componentDidMount() {
    ReactDOM.findDOMNode(this).addEventListener('click', this.onClick);
  }
  
  componentWillUnmount() {
    ReactDOM.findDOMNode(this).removeEventListener('click', this.onClick);
  }
  
  onClick = (e) => {
    if (e.target.tagName === 'A') { // Naive check for <a> elements
      sendAnalytics('link clicked', {
        documentId: this.props.documentId // Specific information to be sent
      });
    }
  };
  
  render() {
    // ...

這麼作的幾個問題在於:

  • 相關組件 Document 除了自身的主要邏輯:顯示主頁面以外,多了其餘統計邏輯;

  • 若是 Document 組件的生命週期函數中,還存在其餘邏輯,那麼這個組件就會變的更加含糊不合理;

  • 統計邏輯代碼沒法複用;

  • 組件重構、維護都會變的更加困難。

爲了解決這個問題,咱們提出了高階組件這個概念: higher-order components (HOCs)。不去晦澀地解釋這個名詞,咱們來直接看看使用高階組件如何來重構上面的代碼:

function withLinkAnalytics(mapPropsToData, WrappedComponent) {
  class LinkAnalyticsWrapper extends React.Component {
    componentDidMount() {
      ReactDOM.findDOMNode(this).addEventListener('click', this.onClick);
    }

    componentWillUnmount() {
      ReactDOM.findDOMNode(this).removeEventListener('click', this.onClick);
    }

    onClick = (e) => {
      if (e.target.tagName === 'A') { // Naive check for <a> elements
        const data = mapPropsToData ? mapPropsToData(this.props) : {};
        sendAnalytics('link clicked', data);
      }
    };
    
    render() {
      // Simply render the WrappedComponent with all props
      return <WrappedComponent {...this.props} />;
    }
  }

須要注意的是,withLinkAnalytics 函數並不會去改變 WrappedComponent 組件自己,更不會去改變 WrappedComponent 組件的行爲。而是返回了一個被包裹的新組件。實際用法爲:

class Document extends React.Component {
  render() {
    // ...
  }
}

export default withLinkAnalytics((props) => ({
  documentId: props.documentId
}), Document);

這樣一來,Document 組件仍然只需關心本身該關心的部分,而 withLinkAnalytics 賦予了複用統計邏輯的能力。

高階組件的存在,完美展現了 React 天生的複合(compositional)能力,在 React 社區當中,react-redux,styled-components,react-intl 等都廣泛採用了這個方式。值得一提的是,recompose 類庫又利用高階組件,併發揚光大,作到了「腦洞大開」的事情。

總結

React 及其周邊社區的崛起,讓函數式編程風靡一時,受到追捧。其中關於 decomposing 和 composing 的思想,我認爲很是值得學習。同時,對開發設計的一個建議是,不要猶豫將你的組件拆分的更小、更單一,由於這樣能換來強健和複用。

本文意譯了David Tang的:Techniques for decomposing React components一文。

Happy Coding!

PS: 做者Github倉庫,歡迎經過代碼各類形式交流。

相關文章
相關標籤/搜索