隨著 React 在前端開發中越來越流行,各種各樣的設計模式及新概念亦層出不窮。本文旨在總結 React 開發中一些常見的設計模式。
有狀態 (Stateful) vs 無狀態 (stateless)
React 元件可以是有狀態的,在其生命週期內可以操縱並改變其內部狀態;React 元件也可以是無狀態的,它僅接受來自父元件傳入的 props,並進行展示。
下面是一個無狀態的 Button
元件,它的行為完全由傳入的 props 決定:
const Button = props =>
<button onClick={props.onClick}>
{props.text}
</button>
複製程式碼
下面是一個有狀態元件(使用了上述的無狀態元件):
class ButtonCounter extends React.Component {
constructor() {
super();
this.state = { clicks: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ clicks: ++this.state.clicks });
}
render() {
return (
<Button
onClick={this.handleClick}
text={`You've clicked me ${this.state.clicks} times !`}
/>
)
}
}
複製程式碼
正如你所看到的,上述 ButtonCounter
元件在 state
中維護了自己的狀態,而之前的 Button
元件僅根據 props 來進行渲染展示。這個區別看似很小,但是無狀態的 Button
元件卻高度可複用。
容器(Container) vs 展示(Presentational) 元件
當與外部資料進行互動時,我們可以把元件分為兩類:
- 容器元件:主要負責同外部資料進行互動(通訊),譬如與 Redux 等進行資料繫結等。
- 展示元件:根據自身 state 及接收自父元件的 props 做渲染,並不直接與外部資料來源進行溝通。
我們來看一個展示元件:
const UserList = props =>
<ul>
{props.users.map(u => (
<li>{u.name} - {u.age} years old</li>
))}
</ul>
複製程式碼
而這個展示元件可以被一個容器元件更新:
class UserListContainer extends React.Component {
constructor() {
super()
this.state = { users: [] }
}
componentDidMount() {
fetchUsers(users => this.setState({ users }));
}
render() {
return <UserList users={this.state.users} />
}
}
複製程式碼
通過將元件區分為容器元件與展示元件,將資料獲取與渲染進行分離。這也使 UserList
可複用。如果你想了解更多,這裡有一些非常好的文章,解釋地非常清楚。
高階(Higher-Order)元件
當你想複用一個元件的邏輯時,高階元件(HOC)就派上用場了。高階元件就是 JavaScript 函式,接收 React 元件作為引數,並返回一個新元件。
舉個例子:編寫一個選單元件,當點選一個選單項時,展開當前選單項,顯示子選單。當然我們可以在父元件裡來控制此選單元件的狀態,但是更優雅的方式,是使用高階元件:
function makeToggleable(Clickable) {
return class extends React.Component {
constructor() {
super();
this.toggle = this.toggle.bind(this);
this.state = { show: false };
}
toggle() {
this.setState({ show: !this.state.show });
}
render() {
return (
<div>
<Clickable
{...this.props}
onClick={this.toggle}
/>
{this.state.show && this.props.children}
</div>
);
}
}
}
複製程式碼
通過這種方式,我們可以使用 JavaScript 的裝飾器語法,將我們的邏輯應用於 ToggleableMenu
元件:
@makeToggleable
class ToggleableMenu extends React.Component {
render() {
return (
<div onClick={this.props.onClick}>
<h1>{this.props.title}</h1>
</div>
);
}
}
複製程式碼
現在,我們可以將任何子選單內容放入 ToggleableMenu
元件中:
class Menu extends React.Component {
render() {
return (
<div>
<ToggleableMenu title="First Menu">
<p>Some content</p>
</ToggleableMenu>
<ToggleableMenu title="Second Menu">
<p>Another content</p>
</ToggleableMenu>
<ToggleableMenu title="Third Menu">
<p>More content</p>
</ToggleableMenu>
</div>
);
}
}
複製程式碼
當你在使用 Redux 的 connect
,或者 React Router 的 withRouter
函式時,你就是在使用高階元件!
渲染回撥(Render Callbacks)
除了上述的高階元件外,渲染回撥是另一種使元件可複用的設計模式。渲染回撥的核心是元件接收的子元件(或子結點,亦即 props.children
),不以 React Component
提供,而是以回撥函式的形式提供。以上述 HOC 元件為例,我們通過渲染回撥的方式重寫如下:
class Toggleable extends React.Component {
constructor() {
super();
this.toggle = this.toggle.bind(this);
this.state = { show: false }
}
toggle() {
this.setState({ show: !this.state.show });
}
render() {
return this.props.children(this.state.show, this.toggle)
}
}
複製程式碼
現在,我們可以傳入回撥函式給 Toggleable
元件作為子結點。 我們用新方式實現之前的 HOC 元件 ToggleableMenu
:
const ToggleableMenu = props => (
<Toggleable>
{(show, onClick) => (
<div>
<div onClick={onClick}>
<h1>{props.title}</h1>
</div>
{ show && props.children }
</div>
)}
</Toggleable>
)
複製程式碼
而我們全新的 Menu
元件實現如下:
class Menu extends React.Component {
render() {
return (
<div>
<ToggleableMenu title="First Menu">
<p>Some content</p>
</ToggleableMenu>
<ToggleableMenu title="Second Menu">
<p>Another content</p>
</ToggleableMenu>
<ToggleableMenu title="Third Menu">
<p>More content</p>
</ToggleableMenu>
</div>
);
}
}
複製程式碼
是的,你沒有看錯,新的 Menu
元件同之前以HOC模式實現出來的一模一樣!
在這種實現方式下,我們將元件內部的狀態(state
)與元件的渲染邏輯進行剝離。在上面的例子中,我們將渲染邏輯放在了 ToggleableMenu
的渲染回撥中,而展示元件的狀態(state
)依然在 Toggleable
元件內進行維護。
瞭解更多
以上的一些例子僅僅是 React 設計模式的基礎知識。如果你想更加深入地瞭解關於 React 設計模式的話題,以下是一些非常好的學習資料,值得一看:
- React Component Patterns by Michael Chan
- React Patterns
- Presentational and Container Components
- React Higher Order Components in depth
- Function as Child Components
- Recompose
- Downshift
關注微信公眾號:創宇前端(KnownsecFED),碼上獲取更多優質乾貨!