寫React應用時,我發現了一種簡單而有效的模式。如果你也寫過一陣子React,或許你也已經發現它了。對於這種模式,這篇文章講得不錯,不過我還想再補充幾點。
如果把組件 分爲以下兩類,對組件的複用和理解會更容易一些。我這兩類組件稱爲 展示組件 和 容器組件。也有叫“胖的&瘦的”、“聰明的&笨的”、“包含狀態的的&純的”、“Screens and Components”的等等說法,這些說法並不完全一致,但核心理念大概相同。
展示組件的特性:
- 負責外觀的展示
- 可能同時包含展示組件 & 容器組件,通常帶有自身的 DOM標籤 和 樣式屬性
- 通常可以通過
this.props.children
包含組件 - 不依賴應用中的其他組件,如FLUX的 actions 或 stores
- 對於如何加載、修改數據,不做具體規定
- 僅通過props來接收數據 和 回調函數
- 幾乎沒有自身的狀態(就是有,也是UI狀態,而不是數據)
- 除非組件需要狀態、生命週期鉤子(lifecycle hooks)、或者性能優化,否則一般寫爲函數式組件
- 例如 Page, Sidebar, Story, UserInfo, List.
容器組件的特性
- 負責功能的實現
- 可能同時包含展示組件 & 容器組件,但通常自身不帶有任何DOM標籤(起包裹作用的div除外),也不帶有任何樣式屬性
- 向 展示組件 和 其他容器組件 提供數據和行爲/方法
- 調用Flux的actions,並將其作爲回調函數,提供給展示組件
- 通常是包含狀態的,因爲經常把它們作爲數據源使用。
- 通常不是手寫的,而是用高階組件生成的。(高階組件如React Redux 的
connect()
,Relay的createContainer()
或者 Flux Utils的Container.create()
等) - 例如:UserPage, FollowersSidebar, StoryContainer, FollowedUserList.
爲了讓這種區分更加明顯,我會把這兩類組件放到不同的目錄裏。
這麼做的好處
- 兩類組件各司其職。由此,你對該APP/UI的理解會更加深入。
- 更好的複用性。對完全不同的狀態源,你可以使用同一個展示組件,並將其變爲不同的、可進一步複用的容器組件。
- 展示組件其實就是APP的“調色板”。你可以把它們放到一個單獨的頁面上,交給設計師,隨便他怎麼折騰,APP的邏輯和功能都不會受到一絲影響。你可以在這個頁面上進行screenshot regression測試。
- 迫使你從APP裏“提煉”出“佈局組件”,如Sidebar, Page, ContextMenu等。由此,你將不得不使用this.props.children,而非在若干容器組件內 重複使用一套佈局相關的代碼。
請注意,組件並不一定需要生成DOM。它們只需要提供UI之間的分界與組合關係。
好好利用這一點。
何時引入容器組件?
在剛開始寫APP的時候,我建議你只寫展示組件。先就這麼寫着,總會有一個時刻,你將注意到,有太多的屬性需要傳遞給中間層的組件。有些組件根本用不上這些屬性,傳給它們的目的,僅僅是爲了能繼續向下傳遞屬性。而且,當子組件需要更多的數據時,你不得不重寫中間層的組件。當你意識到這些時,就是引入容器組件的好時機。通過使用容器組件,無需途徑組件樹中其他無關的組件,就可以直接將數據和方法屬性傳給末端的葉子組件中。
這種重構的過程是漸進的,別想着一步到位。隨着你對這種模式日復一日地練習,對於何時使用容器組件,你會慢慢培養出一種直覺。這種感覺就像你知道啥時候應該抽象出函數一樣。我在蛋頭網(egghead)上的免費系列課程也於此會有所幫助。
其他的分類方法
需要注意的是,展示組件 和 容器組件 之間的區別,並非是技術上的,而是在用途上的。理解這一點很重要。
作爲對比,這裏列舉一些相關(但不同)的技術上的區別
有狀態和無狀態。有的組件使用 React 的 setState() 方法,有的組件則不用。儘管容器組件多是有狀態的,展示組件多是無狀態的,但是這並非硬性規定。展示組件也可以是有狀態的,容器組件也可以是無狀態的。
類和函數。 從 React 0.14 開始 ,組件既可以聲明爲類,也可以聲明爲函數。雖然函數式組件更容易定義,但是它們缺乏某些當前只有類組件纔有的功能。在未來,這些限制可能會漸漸取消,但是目前確實是存在的。因爲函數式組件更容易理解,我建議你一般用函數式組件就好,除非你需要狀態、生命週期鉤子或性能優化等目前 類組件獨有的功能。
純和不純。有人說,只要拿到相同的屬性(props)和狀態,就能返回相同的結果,那麼該組件就是純組件。純組件既可以被定義爲類,也可以被定義爲函數,既可以是有狀態,也可以是無狀態的。純組件的另一個重要特徵是,它們不會依賴於屬性(props) 或者狀態的深層變化(deep mutations),所以它們的渲染性能可以在 shouldComponentUpdate() 鉤子中通過 shallow comparison 來優化。目前只有類可以定義
shouldComponentUpdate()
,以後可能會放寬限制。
不論是展示組件,還是容器組件,都可能是上面所列舉的任意一種。以我的經驗看,展示組件多是無狀態的純函數,而容器組件多是有狀態的純類。不過,這並非規定,而是經驗之談。我確實見過完全相反,但在特定條件下成立的例子。
別把展示組件/容器組件的這種分類方法視作教條。有時候其實無所謂,有時候又難以分辨。如果你對某個組件屬於展示組件還是容器組件舉棋不定,別急,或許還沒到下結論的時候。
例子
Michael Chan的這一篇真的說到點子上了。
延伸閱讀
- Getting Started with Redux
- Mixins are Dead, Long Live Composition
- Container Components
- Atomic Web Design
- Building the Facebook News Feed with Relay
譯者注
注1
一個具體的組件拆分案例
在作者推薦的Michael Chan的這一篇文章裏,有一個具體的例子,對理解本文頗有脾益,摘錄於下:
A component like this would be rejected in code review for having both a presentation and data concern:
// CommentList.js
class CommentList extends React.Component {
constructor(props) {
super(props);
this.state = { comments: [] }
}
componentDidMount() {
$.ajax({
url: "/my-comments.json",
dataType: 'json',
success: function(comments) {
this.setState({comments: comments});
}.bind(this)
});
}
render() {
return <ul> {this.state.comments.map(renderComment)} </ul>;
}
renderComment({body, author}) {
return <li>{body}—{author}</li>;
}
}
It would then be split into two components. The first is like a traditional template, concerned only with presentation, and the second is tasked with fetching data and rendering the related view component.
// CommentList.js
class CommentList extends React.Component {
constructor(props) {
super(props);
}
render() {
return <ul> {this.props.comments.map(renderComment)} </ul>;
}
renderComment({body, author}) {
return <li>{body}—{author}</li>;
}
}
// CommentListContainer.js
class CommentListContainer extends React.Component {
constructor() {
super();
this.state = { comments: [] }
}
componentDidMount() {
$.ajax({
url: "/my-comments.json",
dataType: 'json',
success: function(comments) {
this.setState({comments: comments});
}.bind(this)
});
}
render() {
return <CommentList comments={this.state.comments} />;
}
}
In the updated example, CommentListContainer could shed JSX pretty simply.
render() {
return React.createElement(CommentList, { comments: this.state.comments });
}
注2
Dan結合案例講解這兩種組件的視頻課
Redux: Extracting Presentational Components (Todo, TodoList)
Redux: Extracting Presentational Components (AddTodo, Footer, FilterLink)
Redux: Extracting Container Components (VisibleTodoList, AddTodo)
注3