设计合适的State
组件state必须能够代表一个组件UI呈现的完整状态集,即组件的任何UI改变都可以从state的变化中反映出来;
同时,state还必须代表一个组件UI呈现的最小状态集,即state中的所有状态都用于反映组件UI的变化,没有任何多余的状态,也不应该存在通过其他状态计算而来的中间状态。
state所代表的一个组件UI呈现的完整状态集又可以分为两类数据:
- 用作渲染组件时用到的数据的来源;
- 用于组件UI展现形式的判断依据;
示例代码:
import React from 'react';
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {
user: 'React',
display: true
}
}
render() {
return(
<div>
{
this.state.display ? <h1>你好,{this.state.user}</h1> : null
}
</div>
);
}
}
export default Hello;
state还容易和props以及组件的普通属性混淆。
在ES 6中,可以使用this.{属性名}定义一个class的属性,也可以说属性是直接挂载到this下的变量。
因此,state、props实际上也是组件的属性,只不过是React在Component中已经预设。
除了props和state以外的其他组件属性称之为组件的普通属性。
假设一个组件需要显示当前时间,并且这个时间每秒都会自动更新,这个组件内就需要定义一个计时器,在这个计时器中每隔1s更新一次组件的state。这个计时器变量并不适合定义在组件的state中,因为它并不代表组件UI呈现状态,它只是用来改变组件的state,这个时候就到了组件的普通属性发挥作用的时候。
示例代码:
import React from 'react';
class NewHello extends React.Component {
constructor(props) {
super(props);
//定义普通属性
this.timer = null;
this.state = {
date : new Date()
}
this.updateDate = this.updateDate.bind(this);
}
componentDidMount() {
this.timer = setInterval(this.updateDate, 1000);
}
componentWillUnmount() {
clearInterval(this.timer);
}
updateDate() {
this.setState({
date: new Date()
});
}
render() {
return(
<div>
<h1>当前时间:{this.state.date.toString()}</h1>
</div>
);
}
}
export default NewHello;
当在组件中需要用到一个变量,并且它与组件的渲染无关时,就应该把这个变量定义为组件的普通属性,直接挂载到this下,而不是作为组件的state。还有个更加直观的判断方法:组件的render方法有没有用到这个变量,如果没有用到,那么这就是个普通属性。
props和state的区别是什么?state和props都直接和组件的UI渲染有关,它们的变化都会触发组件的重新渲染,但是props对于使用它的组件来说是只读的,是通过父组件传递过来的,要想修改props只能在父组件中修改,而state是组件内部自己维护的状态,是可变的。
组件中用到的一个变量是不是应该作为state,可以通过以下依据进行判断:
- 此变量是否通过props从父组件获取
- 如果是,那么此变量不是一个state;
- 此变量是否在组件的整个生命周期都保持不变
- 如果是,那么此变量不是一个state;
- 此变量是否可以通过其他state或者props计算得到
- 如果是,那么此变量不是一个state;
- 此变量是否在组件的render方法中使用
- 如果不是,那么此变量不是一个state;
正确修改state
state可以通过this.state.{属性}的方式直接获取,当修改state时需要注意:
-
不能直接修改state
直接修改state并不会触发组件重新渲染
this.state.title = 'new value';
需要用setState()方法才可以触发重新渲染
this.setState({title: 'new value'});
-
state的更新是异步的
调用setState时,组件的state并不会立即改变,setState只是会把要修改的状态放入一个队列中,React会优化真正的执行时机,并且处于性能原因,可能会将多次setState的状态修改合并成一次修改状态。
所以不要依赖当前的state,计算下一个state。
当真正执行状态修改时,依赖的this.state并不能保证是最新的state,因为React会把多次state的修改合并成一次,这时this.state还是这几次state修改前的state。
也不能依赖当前props计算下一个state,因为props也是异步更新的。
-
state的更新是一个合并的过程
当调用setState修改组件的状态时,只需要传入发生改变的state即可,而不是组件完整的state,因为组件state的更新过程是一个合并的过程。
state与不可变对象
React官方建议把state当作不可变对象,一方面,直接修改this.state,组件并不会重新render,另一方面,state中包含的所有状态都应该是不可变的对象。
当state中的某个状态发生改变时,应该重新创建这个状态对象,而不是直接修改原来的对象。
根据状态的类型可以分为以下三类:
-
状态的类型是不可变类型
不可变类型包括:数字、字符串、布尔值、null和undefined;
因为是不可变类型,因此可以直接给要修改的状态赋新值。
-
状态的类型是数组
像数组中添加元素时:
使用preState、concat创建新的数组:
this.setState(preState => ({ values: preState.values.concat([newValue]) }));
使用ES 6 spread syntax:
this.setState(preState => ({ value: [...preState.values, newValue] }));
截取数组中部分元素时,使用数组的slice方法:
this.setState(preState => ({ value: preState.values.slice(startIndex, endIndex) }));
过滤数组中部分元素时,可以使用数组的filter方法:
this.setState(preState => ({ value: preState.values.filter(item => { return item !== aimValue }) }));
-
状态的类型是普通对象
普通对象不包括字符串和数组。
使用ES 6的Object.assgin方法:
this.setState(preState => ({ value: Object.assgin(targetObj, preState.obj, sourceObj) }));
使用对象扩展语法:
this.setState(preState => ({ value: {...preState.obj, aimAttr: newValue} }));
为什么React推荐组件的状态是不可变对象呢?
一方面是因为对不可变对象的修改会返回一个新的对象,不需要担心原有对象在不小心的情况下被修改导致的错误,方便程序的管理和调试;
另一方面是因为处于性能考虑,当对象组件状态都是不可变对象时,在组件的shouldComponentUpdate方法中仅需要比较前后两次state对象的引用就可以判断state是否真的改变,从而避免不必要的render调用。