当我们写React应用的时候,知道在组件中何时使用state何时不使用state,是非常重要的。在这篇文章中,我将回顾我所认为的使用state的最佳实践:
- 如果component没有自己的数据,那么其他数据便不应该影响它的state。
- 用于描述组件的state尽可能简单。
- 运算和条件判断移动到render函数。
这些规则如果有特殊情况,应该在适当的时候违反。不过如果你能够一直都遵循它们的话,你会发现你的component更容易解耦,测试更容易写,而且整个应用的bug也很少。下面让我们仔细看看这些规则:
1. 如果component没有自己的数据,那么其他数据便不应该影响它的state
第一,可能是最重要的一点,component的state不应该依赖于props传递。当然props可能向子组件传递state,例如,在一个普通的input组件中,为了禁用input的文字输入,我可能选择一个disabled的prop。但是当我说'state'的时候,我是明确的指component的state属性。所以,当state开始依赖于它的props的时候,你可能会发现这是一段不好的代码。看看以下代码片段:
import React from 'react';
class UserWidget extends React.Component {
// BAD: 通过props接收到的值设置this.state.fullName
constructor (props) {
this.state = {
fullName: `${props.firstName} ${props.lastName}`
};
}
render () {
var fullName = this.state.fullName;
var picture = this.props.picture;
return (
<div>
<img src={picture} />
<h2>{fullName}</h2>
</div>
);
}
}
以上代码有什么问题?一开始可能不是很明显,但是假如firstName或者lastName改变了,UserWidget组件的视图将不会改变。构造函数在组件初始化渲染执行之后只会调用一次,因此fullName的值永远是第一次渲染时候的值。React新手可能经常会犯这样的错误,因为setState是更新组件视图的最简单且最明显的方式。
你应该问问你自己,该组件是否拥有这些数据,内部的firstName和lastName创建了吗?如果没有,那么state不应该依赖便不应该依赖于这些数据。那么最好的避免这个问题的方式是什么呢?在render函数里面计算fullName的值。
render () {
var fullName = `${this.props.firstName} ${this.props.lastName}`;
// ...
}
把fullName移动到render函数里面之后,我们将不用再关心fullName的值是否更新了。当props改变的时候,React会运行一个钩子函数--componentWillReceiveProps,然而我还是会考虑这种反模式,因为它不需要增加项目的复杂性。
当然,如果你在组件初始化之后不关心props,那么这条规则将不会适用。
当使用React.createClass代替extends React.Component时候,则用getInitialState代替constructor。
有时候,"state"将需要设置一些值,在flux模式中,可能是根控制器组件监听不同的stores。
2. 用于描述组件的state尽可能简单
你应该尽可能的简单的去描述一个组件的状态。在很多种情况下,这意味着用布尔值是更好的方式。
思考下面的例子,我们有一些组件,它们在state里面的class属性是基于clicked和hovered事件改变的。(不管你信不信,我看到过很多这样的例子)
import React from 'react';
var cx = React.addons.classSet;
class ArbitraryWidget extends React.Component {
constructor() {
this.state = {
classes: []
};
}
// BAD: 当鼠标滑过的时候,把'hover'push到this.state.classes
handleMouseOver() {
var classes = this.state.classes;
classes.push('hover');
this.setState({ classes: classes });
}
// BAD: 当鼠标离开的时候,从this.state.classes移除'hover'
handleMouseOut() {
var classes = this.state.classes;
var index = classes.indexOf('hover');
classes.splice(index, 1);
this.setState({ classes: classes });
}
// BAD: 被点击的时候,在this.state.classes切换'active'
handleClick() {
var classes = this.state.classes;
var index = classes.indexOf('active');
if (index != -1) {
classes.splice(index, 1);
} else {
classes.push('active');
}
this.setState({ classes: classes });
}
render() {
var classes = this.state.classes;
return (
<div className={cx(classes)}
onClick={this.handleClick.bind(this)}
onMouseOver={this.handleMouseOver.bind(this)}
onMouseOut={this.handleMouseOut.bind(this)}
/>
)
}
}
这个组件可以运行,但是我持保留意见。它现在的state是一个存着字符串类型的数组,this.state.classes = ['active', 'hover']
,不仅代码的可读性很差,而且改变起来特别麻烦。假如有其他组件依赖于我的这个class的数组,那么查看这个数组是否包含hover
肯定比查看hover
的布尔值是什么的难度要大。我们需要重构这段代码,用布尔值代表组件是否应该有这些class,例如isHovering === true
意味着我是否应该使用hover这个class。
import React from 'react';
var cx = React.addons.classSet;
class ArbitraryWidget extends React.Component {
constructor() {
this.state = {
isHovering: false,
isActive: false
};
}
// GOOD: 当鼠标滑过的时候,this.state.isHovering设置为true
handleMouseOver() {
this.setState({ isHovering: true });
}
// GOOD: 当鼠标离开的时候,this.state.isHovering设置为false
handleMouseOut() {
this.setState({ isHovering: false });
}
// GOOD: 被点击的时候,改变this.state.active
handleClick() {
var active = !this.state.isActive;
this.setState({ isActive: active });
}
render() {
// use the classSet addon to concat an array of class names together
var classes = cx([
this.state.isHovering && 'hover',
this.state.isActive && 'active'
]);
return (
<div className={cx(classes)}
onClick={this.handleClick.bind(this)}
onMouseOver={this.handleMouseOver.bind(this)}
onMouseOut={this.handleMouseOut.bind(this)}
/>
);
}
}
为了使用这些state的布尔值,我们必须在render函数里面计算class数组。但是,我们增强了代码的可读性,this.state.isHovering
远比this.state.classes.indexOf('hover') != -1
更能代表组件实际的状态。这个组件更容易扩展和测试,因为我们不需要考虑数组的构建。
我想再说一遍,你应该始终以用最简单的方式表示state为目标。这并不一定意味着你只能存储布尔值,有可能是深层嵌套的对象,也可能是数字、字符串或者函数。
想象一下作为其他人,试图观察组件返回的一个class的数组的状态,这个数组对于你是否有用呢?当然没有。相比之下布尔值isActive
是更为可行的。我希望你明白我的意思。
3. 运算和条件判移动到render函数
在前面的两条规则中,这一条其实已经提到了。然而,它仍然是值得注意的。尽可能的在render函数中进行最后一步运算。虽然这样也许会略慢于其他方法,但它能确保最少的重定向组件,在轻微的性能提升之前,我们应该更注重代码的可读性和扩展性。
我需要连接prop中的firstName和lastName?把它移动到render函数。我的组件需要使用哪个class?在render函数中做决定。如果我的todo列表没有任何项目,我应该显示在text框中显示一个placeholder?在render函数中做决定。我需要格式化电话号码?在render函数中做决定。我该如何呈现出子组件?在render函数中做决定。我今天要吃午饭吗?在render函数中做决定。
当然,你不要把所有代码都放在一个函数里面。相反,最好把它们分割成合适的helper函数(用一个好的名字),关键是你用render函数做太多的事情的话,应该减少它的复杂性。你可以用一个前缀来表示helper函数。例如:
// GOOD: Helper function to render fullName
renderFullName () {
return `${this.props.firstName} ${this.props.lastName}`;
}
render () {
var fullName = this.renderFullName();
// ...
}
CPU密集运算
因为我建议你把所有的东西都推迟到render函数中,它会导致CPU密集运算也会推迟。为了避免重复复杂的渲染,考虑memoization的功能。
不要把变量存储到component实例上
不要像下面这样做:
class ArbitraryWidget extends React.Component {
constructor () {
this.derp = 'something';
}
handleClick () {
this.derp = 'somethingElse';
}
render () {
var something = this.derp;
}
}
这是非常不好的,不仅是因为你没有遵守用this.state
存储值的约定,而且this.derp
改变的时候,不会自动触发render。
网友评论