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