随着 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),码上获取更多优质干货!