以下是使用 React 的一些最佳实践总结,参考自 https://github.com/planningcenter/react-patterns 并结合一些我的实践认识总结。

组件通信

每个 React 组件都有自己的状态,输入、输出,本质上每个组件都可以看作一个小的系统。对于组件来说,其输入即 props。对于一个典型的 React 组件来说,我们需要对其 props 作一定的约束(非强制,但推荐),如定义其 propTypes、defaultProps 等。props 中有一个比较特殊的字段 children,我们可以拿到其子组件,如下面的代码中,<span className=“notation"></span> 即相应 Label 实例的 props.children

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Label extends React.Component {
static propTypes = {
msg: React.PropTypes.string.isRequired
}
static defaultProps = {
msg: ''
}
render() {
return (
<span></span>
)
}
};
<Label><span className="notation"></span></Label>

但需要注意的是,我们无法获得组件的状态等信息,React 中更推崇单向数据流。而 React 的交互也是通过 props 来达到,通过将交互相关的回调函数绑定到对应的 onXXX 事件上即可以达到。

组合

React 最大的优点在于其高度组件化,我们可以通过像搭积木的方式将各组件拼接起来,形成一个应用。但是,对于父子组件间的组合,有一点需要特别注意:

比如一个页面,由头部、内容、尾部组合而成,头部又由标题区、导航等等组合,导航可能又由别的东西组合而成,用 React 描述这层关系的时候,很容易写出下面这类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends React.Component {
render() {
return (
<div>
<Header />
<Main />
<Footer />
</div>
);
}
}
class Header extends React.Component {
render() {
return (
<div>
<Title title=“xxx” />
<Navigation />
</div>
);
}
}

看起来,好像这样的代码很合理,组件职责划分也很清晰。但是,这样的代码很容易出现一个问题,即组件间的解耦其实并没有做到特别独立。想象一下,如果某个页面不需要导航,我们是应该到 Header 中还是在 App 中修改呢?

更合理的是通过使用 this.props.children 将子组件与父组件解耦出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default class App extends React.Component {
render() {
return (
<Header>
<Navigation />
</Header>
);
}
}
export default class Header extends React.Component {
render() {
return <header>{ this.props.children }</header>;
}
};

依赖注入

如果数据从上层组件传递到子组件,即便中间组件不需要用到这个数据,这整条链路中的涉及到的组件都需要在 props 设定对应的值以便能够传递下去。

有一种避免这个问题的方式是通过使用 context 来达到。

但 FB 其实是不推荐过度使用 context 的,参考这里

事件绑定

我们知道,如果使用类的方式创建组件,React 是不支持对方法进行 this 作用域自动绑定的。所以我们可能不得不在代码中出现下面这种代码:

1
<button onClick={this.handler.bind(this)}></button>

但是,真的有必要这么做么?要知道,React 每次 render 的时候,都会导致重新调用 bind 生成一个新的函数。

一个更好的方法是在类的构造函数中进行 bind,即:

1
2
3
4
5
constructor(props) {
super(props);
// ...
this.handler = this.handler.bind(this);
}

容器组件

React 实践过程中,我们常常纠结于数据应该放在什么地方比较好,是通过 props 传递,还是通过 state 来由组件自身管理。一个比较好的实践模式是使用高阶组件将组件分离成表示层与容器层。

什么意思呢?举个例子:

比如我们要实现一个定时器的组件功能,我们通过会把定时、setInterval、渲染等等工作全放在一个 Timer 组件中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {
tic: props.time
}
}
componentDidMount() {
this.interval = setInterval(() => {
this.update();
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
update() {
this.setState({
tic: this.state.tic + 1000
});
}
_formatTime(time) {
time = new Date(time);
var [ hours, minutes, seconds ] = [
time.getHours(),
time.getMinutes(),
time.getSeconds()
].map(num => num < 10 ? '0' + num : num);
return { hours, minutes, seconds };
}
render() {
const time = this._formatTime(this.state.tic);
return (
<h1>{ time.hours } : { time.minutes } : { time.seconds }</h1>
);
}
};

乍一看,上面的代码直观明了,但是,从代码设计的角度来看,上面的代码在职责分离上还是有一些问题:

  1. 状态由 Timer 组件自身管理,导致当前 tic 值只有 Timer 实例自己知道,对于外部其它组件,这个值无法被共享读取

那么,如何使用高阶组件对它进行拆分解耦呢?

TODO:https://medium.com/@learnreact/container-components-c0e67432e005

高阶组件

高阶组件可以看作是修饰者模式在 React 中的运用。它对组件进行封装并做一些额外的事情,外界可以无感知地认为是调用原始组件。一般而言,都是通过导出一个工厂方法,接收原始组件作为参数,返回出一个新的组件。对于外部的调用者,使用的其实是新的组件。

好处在于,组件可以更加独立,高阶组件对于额外配置、测试 mock 等是一个非常方便的手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
var makeIdentityComponent = (Component) =>
class Identity extends React.Component {
render() {
return (
<Component
{...this.state}
{...this.props}
/>
)
}
};
export default makeIdentityComponent;

FLUX 与单向数据流

React 推崇单向数据流的架构,其优点在于:

  1. 数据的统一管理
  2. 自顶向下的更新使得整个代码结构更加清晰
  3. 如果每个组件自己去管理数据的话,其状态数据很难暴露出来,只能通过调用 this.props.callback 的方式将其状态变化值再反向回馈到父组件上,引起相应的父组件状态变化。或者只能使用类似于 pub/sub 的方式使得整个应用越来越复杂

单向数据流使得组件扮演的只是表示层的角色,不再做更多的服务层的事情。

1
2
3
4
5
6
7
8
9
10
11
12
后端服务
^
|
v
数据工厂 <-----
| |
v |
组件 ----->
^
|
|
交互