React.js: Higher-Order Components (HOC)
20 May 2018Higher-Order Components(HOC)是一個函數,可代入元件(Component)作為參數,並回傳一個新的元件。使用 HOC 的目的是將通用的邏輯放在 HOC 中,變動的部分就由代入 Component 的 props 和 state 傳入即可。
簡易範例可參考這裡。
範例
這是我的 Side Project「吃什麼,どっち」,主要用於推薦餐廳。其中,右側動態消息「有什麼新鮮事?」(Feeds,紫框處),且每個商店資訊的下方會顯示評價(Comments,藍框處)。Feeds 和 Comments 的功能很類似,都會展示一連串的資料,並且當使用者按下「看更多」按鈕時會顯示更多的項目。
HOC
HOC 接收一個元件(WrappedComponent)、狀態(state)等,這裡除了接收元件和狀態外,還有提取更多項目資料的方法(fetchDataFn)。
在此,將 Feeds 和 Comments 的通用邏輯提取出來,放在 HOC 中。它們共通的地方是
- 展示一連串的資料,見程式碼「註一」。
- 當使用者按下「看更多」按鈕時會顯示更多的項目,見程式碼「註二」。
另外,由於每次提取資料要告知後端分頁號碼,因此會在 state 設定一個此元件專用的狀態 pageNum,並在每次提取資料完成後加一。每次提取資料後,會把新拿到的資料與前一份資料合併,當狀態更新重繪畫面時,就會將新拿到的資料附加在舊資料之後。
備註
- 以下程式碼會省略無關的部份並精簡以便說明。
- 使用 Semantic UI React,例如
<Button>
、<Icon>
等。
// HOC.js
import React, { Component } from 'react';
const HOC = (WrappedComponent, state, fetchDataFn) =>
class extends Component {
constructor(props) {
super(props);
this.state = {
...state,
pageNum: 1,
};
this.fetchDataHadler = this.fetchDataHadler.bind(this);
}
fetchDataHadler() {
const fetchedData = fetchDataFn(this.state.pageNum);
this.setState({
pageNum: this.state.pageNum + 1,
data: this.state.data.concat(fetchedData),
});
}
render() {
return (
<div>
<WrappedComponent {...this.props} {...this.state} /> // 註一
<Button onClick={this.fetchDataHadler}>看更多</Button> // 註二
</div>
);
}
};
export default HOC;
Feeds
來看右側動態消息「有什麼新鮮事?」(Feeds),這個元件從父層 props 得到資料並代入樣版產出畫面。
// Feeds.js
import React from 'react';
const Feeds = ({ data }) => {
return(
<Feed>
有什麼新鮮事?
{
_.map(data, (item, index) => {
return (
<Feed.Event key={index}>
<Feed.Label><img src={item.image} /></Feed.Label>
<Feed.Content>
<Feed.Summary>
<Feed.User>{item.name}</Feed.User>在「{item.store.name」享受美食。
<Feed.Date>{item.time}</Feed.Date>
</Feed.Summary>
</Feed.Content>
</Feed.Event>
)
})
}
</Feed>
)
}
export default Feeds;
Comments
來看每個商店資訊的下方會顯示評價(Comments),這個元件從父層 props 得到資料並代入樣版產出畫面。
// Comments.js
import React from 'react';
const Comments = ({ data }) => {
return (
<Comment.Group>
<Header>評價</Header>
{_.map(data, (item, index) => {
return (
<Comment key={index}>
<Comment.Avatar src={item.image} />
<Comment.Content>
{item.name}
<Comment.Metadata>{item.time}</Comment.Metadata>
<Comment.Text>{item.text}</Comment.Text>
</Comment.Content>
</Comment>
);
})}
</Comment.Group>
);
};
export default Comments;
怎麼用
最後來看要怎麼用,這裡分為兩部份
- 在 Main 是主版面,載入元件 Feeds 和 StoreItem(商店資訊)。
- 在 StoreItem 載入元件 Comments。
其中,feedsData 的資料格式範例如下。
const feedsData = {
data: [
{
id: '123456789',
name: '陳秋心',
image: 'http://sample-image/123456789',
time: '今天下午 3:30',
store: {
id: 'abc-123-def-456',
name: 'Grab a Bite 幸福提食'
}
},
...
]
};
// Main.js
import React, { Component } from 'react';
import StoreItem from './components/StoreItem';
import HOC from './components/HOC';
import Feeds from './components/Feeds';
const fetchFeeds = (pageNum = 0) => {
// 取得更多 feeds 資料...
};
const FeedList = HOC(Feeds, feedsData, fetchFeeds);
class StoreList extends Component {
// 省略無關的部份...
// 產出商店資訊
renderStores() {
return _.map(this.props.stores, (store) => {
return <StoreItem key={store.id} store={store} />;
});
}
render() {
return (
<div>
<Header />
<Newsticker news={news} />
{this.renderStores()}
<TagList tags={defaultHotTags} />
<FeedList />
<Footer />
</div>
);
}
}
commentsData 的資料格式範例如下。
const commentsData = {
storeId: store.id,
data: [
{
id: '123456789',
name: '吳艾月',
image: 'http://sample-image/123456789',
time: '今天下午 5:42',
text: '超好吃!'
},
...
]
};
在 StoreItem 載入元件 Comments。
// StoreItem.js
import React from 'react';
import HOC from './components/HOC';
import Comments from './components/Comments';
const StoreItem = ({ store }) => {
// 無關的部份省略...
const fetchComments = () => {
// 取得更多 comments 資料...
};
const CommentList = HOC(Comments, commentsData, fetchComments);
return (
<Item key={store.id}>
<Item.Image src={store.image.url} />
<Item.Content>
<Item.Header>{store.name}</Item.Header>
<Item.Meta>{store.description}</Item.Meta>
<Item.Description>
<ul>
<li>地址:{store.location.address}</li>
<li>電話:{store.phone}</li>
<li>
營業時間:{store.openingHour.start} ~ {store.openingHour.end}
</li>
<li>
價位:{store.price.lowest} ~ {store.price.highest}
</li>
<li>
網站介紹:
<Link to={store.sns.website} title={store.name} target='_blank'>
{store.name}
</Link>
</li>
</ul>
</Item.Description>
<Item.Extra>
{store.tags.map((item, index) => (
<Label key={index}>
<Link to={`/tags/${item}`}>{item}</Link>
</Label>
))}
<CommentList />
</Item.Extra>
</Item.Content>
</Item>
);
};
export default StoreItem;
總結
Higher-Order Components(HOC)是元件邏輯重用的進階技巧,它有以下優點
- 元件必須改變資料來源,只需改變輸入的參數
- 輸入的元件單純化,不做狀態的改變
因此更能做到元件的重用,不必再撰寫相似程式碼了 ♥(´∀` )人