React組件Render Props VS HOC 設計模式

React的設計模式有很多種踪旷,比如無狀態(tài)組件/表現(xiàn)型組件乍丈,有狀態(tài)組件/容器型組件贩挣,render模式組件喉前,高階組件等等。本文主要介紹react的render模式與HOC設計模式王财,并通過實際案例進行比較卵迂。

render props模式

The Render Props是一種在不重復代碼的情況下共享組件間功能的方法。如下所示:

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

通過使用prop來定義呈現(xiàn)的內容搪搏,組件只是注入功能狭握,而不需要知道它如何應用于UI。render prop 模式意味著用戶通過定義單獨組件來傳遞prop方法疯溺,來指示共享組件應該返回的內容论颅。

Render Props 的核心思想是,通過一個函數(shù)將class組件的state作為props傳遞給純函數(shù)組件

import React from 'react';

const SharedComponent extends React.Component {
  state = {...}
  render() {
    return (
      <div>
        {this.props.render(this.state)}
      </div>
    );
  }
}

export default SharedComponent;

this.props.render()是由另外一個組件傳遞過來的囱嫩。為了使用以上組件恃疯,我們可以進行下面的操作:

import React from 'react';
import SharedComponent from 'components/SharedComponent';

const SayHello = () => (
  <SharedComponent render={(state) => (
    <span>hello!,{...state}</span>
  )} />
);

{this.props.render(this.state)}這個函數(shù),將其state作為參數(shù)傳入其的props.render方法中墨闲,調用時直接取組件所需要的state即可今妄。
render props模式最重要的是它返回一個react元素,比如我將上面的render屬性改名,依然有效盾鳞。

import React from 'react';

const SharedComponentWithGoofyName extends React.Component {
  render() {
    return (
      <div>
        {this.props.wrapThisThingInADiv()}
      </div>
    );
  }
}

const SayHelloWithGoofyName = () => (
  <SharedComponentWithGoofyName wrapThisThingInADiv={() => (
    <span>hello!</span>
  )} />
);

HOC設計模式

React的高階組件主要用于組件之間共享通用功能而不重復代碼的模式(也就是達到DRY模式)犬性。

高階組件實際是一個函數(shù)。 HOC函數(shù)將組件作為參數(shù)并返回一個新的組件腾仅。它將組件轉換為另一個組件并添加額外的數(shù)據(jù)或功能乒裆。

高階組件在React生態(tài)鏈技術中經常用到,對讀者較為熟悉的,比如Redux中的connect推励,React Router中的withRouter等鹤耍。

常見的高階組件如下所示:

import React from 'react';

const withSecretToLife = (WrappedComponent) => {
  class HOC extends React.Component {
    render() {
      return (
        <WrappedComponent
          secretToLife={42}
          {...this.props}
        />
      );
    }
  }
    
  return HOC;
};

export default withSecretToLife;

已知secretToLife為42,有一些組件需要共享這個信息验辞,此時創(chuàng)建了SecretToLife的HOC稿黄,將它作為prop傳遞給我們的組件。

import React from 'react';
import withSecretToLife from 'components/withSecretToLife';

const DisplayTheSecret = props => (
  <div>
    The secret to life is {props.secretToLife}.
  </div>
);

const WrappedComponent = withSecretToLife(DisplayTheSecret);

export default WrappedComponent;

此時跌造,WrappedComponent只是DisplayTheSecret的增強版本杆怕,允許我們訪問secretToLife屬性。

Render Props與HOC模式實例對比

本文以一個利用localStorage API的小例子分別使用HOC設計模式跟The Render Props設計模式編寫demo鼻听。

HOC Example

import React from 'react';

const withStorage = (WrappedComponent) => {
  class HOC extends React.Component {
    state = {
      localStorageAvailable: false, 
    };
  
    componentDidMount() {
       this.checkLocalStorageExists();
    }
  
    checkLocalStorageExists() {
      const testKey = 'test';

      try {
          localStorage.setItem(testKey, testKey);
          localStorage.removeItem(testKey);
          this.setState({ localStorageAvailable: true });
      } catch(e) {
          this.setState({ localStorageAvailable: false });
      } 
    }
  
    load = (key) => {
      if (this.state.localStorageAvailable) {
        return localStorage.getItem(key); 
      }
      
      return null;
    }
    
    save = (key, data) => {
      if (this.state.localStorageAvailable) {
        localStorage.setItem(key, data);
      }
    }
    
    remove = (key) => {
      if (this.state.localStorageAvailable) {
        localStorage.removeItem(key);
      }
    }
    
    render() {
      return (
        <WrappedComponent
          load={this.load}
          save={this.save}
          remove={this.remove}
          {...this.props}
        />
      );
    }
  }
    
  return HOC; 
}

export default withStorage;

在withStorage中财著,使用componentDidMount生命周期函數(shù)來檢查checkLocalStorageExists函數(shù)中是否存在localStorage。

local撑碴,save撑教,remove則是來操作localStorage的。現(xiàn)在我們創(chuàng)建一個新的組件醉拓,將其包裹在HOC組件中伟姐,用于顯示相關的信息。由于獲取信息的API調用需要很長時間亿卤,我們可以假設這些值一旦設定就不會改變愤兵。我們只會在未保存值的情況下進行此API調用。 然后排吴,每當用戶返回頁面時秆乳,他們都可以立即訪問數(shù)據(jù),而不是等待我們的API返回钻哩。

import React from 'react';
import withStorage from 'components/withStorage';

class ComponentNeedingStorage extends React.Component {
  state = {
    username: '',
    favoriteMovie: '',
  }

  componentDidMount() {
    const username = this.props.load('username');
    const favoriteMovie = this.props.load('favoriteMovie');
    
    if (!username || !favoriteMovie) {
      // This will come from the parent component
      // and would be passed when we spread props {...this.props}
      this.props.reallyLongApiCall()
        .then((user) => {
          this.props.save('username', user.username) || '';
          this.props.save('favoriteMovie', user.favoriteMovie) || '';
          this.setState({
            username: user.username,
            favoriteMovie: user.favoriteMovie,
          });
        }); 
    } else {
      this.setState({ username, favoriteMovie })
    }
  }

  render() {
    const { username, favoriteMovie } = this.state;
    
    if (!username || !favoriteMovie) {
      return <div>Loading...</div>; 
    }
    
    return (
      <div>
        My username is {username}, and I love to watch {favoriteMovie}.
      </div>
    )
  }
}

const WrappedComponent = withStorage(ComponentNeedingStorage);

export default WrappedComponent;

在封裝組件的componentDidMount內部屹堰,首先嘗試從localStorage中獲取,如果不存在街氢,則異步調用扯键,將獲得的信息存儲到localStorage并顯示出來。

The Render Props Exapmle

import React from 'react';

class Storage extends React.Component {
    state = {
      localStorageAvailable: false, 
    };
  
    componentDidMount() {
       this.checkLocalStorageExists();
    }
  
    checkLocalStorageExists() {
      const testKey = 'test';

      try {
          localStorage.setItem(testKey, testKey);
          localStorage.removeItem(testKey);
          this.setState({ localStorageAvailable: true });
      } catch(e) {
          this.setState({ localStorageAvailable: false });
      } 
    }
  
    load = (key) => {
      if (this.state.localStorageAvailable) {
        return localStorage.getItem(key); 
      }
      
      return null;
    }
    
    save = (key, data) => {
      if (this.state.localStorageAvailable) {
        localStorage.setItem(key, data);
      }
    }
    
    remove = (key) => {
      if (this.state.localStorageAvailable) {
        localStorage.removeItem(key);
      }
    }
    
    render() {
      return (
        <span>
          this.props.render({
            load: this.load,
            save: this.save,
            remove: this.remove,
          })
        </span>
      );
    } 
}

Storage組件內部與HOC的withStorage較為類似珊肃,不同的是Storage不接受組件為參數(shù)荣刑,并且返回this.props.render馅笙。

import React from 'react';
import Storage from 'components/Storage';

class ComponentNeedingStorage extends React.Component {
  state = {
    username: '',
    favoriteMovie: '',
    isFetching: false,
  }

  fetchData = (save) => {
    this.setState({ isFetching: true });
    
    this.props.reallyLongApiCall()
      .then((user) => {
        save('username', user.username);
        save('favoriteMovie', user.favoriteMovie);

        this.setState({
          username: user.username,
          favoriteMovie: user.favoriteMovie,
          isFetching: false,
        });
      }); 
  }

  render() {
    return (
      <Storage
        render={({ load, save, remove }) => {
          const username = load('username') || this.state.username;
          const favoriteMovie = load('favoriteMovie') || this.state.username;
      
          if (!username || !favoriteMovie) {
            if (!this.state.isFetching) {
              this.fetchData(save);               
            }

            return <div>Loading...</div>; 
          }
      
          return (
            <div>
              My username is {username}, and I love to watch {favoriteMovie}.
            </div>
          );
        }}
      />
    )
  }
}

對于ComponentNeedingStorage組件來說,利用了Storage組件的render屬性傳遞的三個方法厉亏,進行一系列的數(shù)據(jù)操作董习,從而展示相關的信息。

render props VS HOC模式

總的來說叶堆,render props其實和高階組件類似阱飘,就是在puru component上增加state,響應react的生命周期虱颗。
對于HOC模式來說,優(yōu)點如下:

  • 支持ES6
  • 復用性強蔗喂,HOC為純函數(shù)且返回值為組件忘渔,可以多層嵌套
  • 支持傳入多個參數(shù),增強了適用范圍

當然也存在如下缺點:

  • 當多個HOC一起使用時缰儿,無法直接判斷子組件的props是哪個HOC負責傳遞的
  • 多個組件嵌套畦粮,容易產生同樣名稱的props
  • HOC可能會產生許多無用的組件,加深了組件的層級

Render Props模式的出現(xiàn)主要是為了解決HOC所出現(xiàn)的問題乖阵。優(yōu)點如下所示:

  • 支持ES6
  • 不用擔心props命名問題宣赔,在render函數(shù)中只取需要的state
  • 不會產生無用的組件加深層級
  • render props模式的構建都是動態(tài)的,所有的改變都在render中觸發(fā)瞪浸,可以更好的利用組件內的生命周期儒将。

當然筆者認為,對于Render Props與HOC兩者的選擇对蒲,應該根據(jù)不同的場景進行選擇钩蚊。Render Props模式比HOC更直觀也更利于調試,而HOC可傳入多個參數(shù)蹈矮,能減少不少的代碼量砰逻。

Render Props對于只讀操作非常適用,如跟蹤屏幕上的滾動位置或鼠標位置泛鸟。 HOC傾向于更好地執(zhí)行更復雜的操作蝠咆,例如以上的localStorage功能。

參考文獻

Understanding React Render Props by Example

Understanding React Higher-Order Components by Example

Ultimate React Component Patterns with Typescript 2.8

React Component Patterns

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末北滥,一起剝皮案震驚了整個濱河市刚操,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碑韵,老刑警劉巖赡茸,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異祝闻,居然都是意外死亡占卧,警方通過查閱死者的電腦和手機遗菠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來华蜒,“玉大人辙纬,你說我怎么就攤上這事“认玻” “怎么了贺拣?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捂蕴。 經常有香客問我譬涡,道長,這世上最難降的妖魔是什么啥辨? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任涡匀,我火速辦了婚禮,結果婚禮上溉知,老公的妹妹穿的比我還像新娘陨瘩。我一直安慰自己,他們只是感情好级乍,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布舌劳。 她就那樣靜靜地躺著,像睡著了一般玫荣。 火紅的嫁衣襯著肌膚如雪甚淡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天崇决,我揣著相機與錄音材诽,去河邊找鬼。 笑死恒傻,一個胖子當著我的面吹牛脸侥,可吹牛的內容都是我干的。 我是一名探鬼主播盈厘,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼睁枕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沸手?” 一聲冷哼從身側響起外遇,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎契吉,沒想到半個月后跳仿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡捐晶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年菲语,在試婚紗的時候發(fā)現(xiàn)自己被綠了妄辩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡山上,死狀恐怖眼耀,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情佩憾,我是刑警寧澤哮伟,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站妄帘,受9級特大地震影響楞黄,放射性物質發(fā)生泄漏。R本人自食惡果不足惜寄摆,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一谅辣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧婶恼,春花似錦、人聲如沸柏副。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽割择。三九已至眷篇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荔泳,已是汗流浹背蕉饼。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留玛歌,地道東北人昧港。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像支子,于是被迫代替她去往敵國和親创肥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容