React 的理念是数据驱动 UI,state 是 React 里面管理数据的重要概念。本文梳理一下操作组件内部 state 的重要 API setState
的用法。
基本使用
每个「会用」React 的开发者都知道更新组件内部的状态时,应该使用 setState
,不过,笔者确实在真实的业务代码中看到过下面的写法:
// Wrong
this.state.comment = 'Hello';
很直接很暴力,动下脑子,如果可以这么写,React 还有必要专门搞一个 setState
的 API 吗?有且只有 constructor
一个例外可以直接给 this.state
赋值。
class SomeComponent extends React.Component {
constructor() {
super();
this.state = {};
}
// ...
}
或者:
class SomeComponent extends React.Component {
// ES6+
state = {};
// ...
}
言归正传,setState
API 接收两个参数:
setState(updater[, callback])
updater
: 必选,我们比较熟悉的形式是一个对象,对象的值为要更新的状态;callback
: 可选,参数更新后的回调。
setState()
把组件的状态变化加入队列,并通知 React 该组件(及其子组件)需要使用新的状态重新渲染。
状态更新是异步的
看到 callback
这个参数,应该自然而然地想到「异步」。
没错 setState()
是个异步操作。基于性能考量,React 不保证 setState()
立即执行,也就是说会有一个延迟,然后再批量更新。
如果需要在状态确实更新以后执行某些操作,可以使用 componentDidUpdate
生命周期方法 或者 setState
回调参数。
还有其他办法保证取到的状态是最新的吗?答案是肯定的,别急,后面会提到的。
合并更新
来看一个例子:
class StateMergeDemo extends React.Component {
state = {
clicked: 0,
};
increaseClickTimes = (id) => {
this.setState({
clicked: this.state.clicked + 1,
}, () => {
console.log(`click ${id}: clicked value -> ${this.state.clicked}`);
});
};
handleClick = () => {
this.increaseClickTimes(1);
this.increaseClickTimes(2);
this.increaseClickTimes(3);
};
render() {
return (
<div>
<p>clicked: {this.state.clicked}</p>
<p onClick={this.handleClick}><button>Click me</button></p>
</div>
)
}
}
ReactDOM.render(<StateMergeDemo />, document.getElementById('root'));
See the Pen React State Updates are Merged by hegfirose (@minwe) on CodePen.
上面的代码中,点击按钮时调用了三次 setState()
,有的同学可能会认为每点击一次,clicked
状态会增加 3,实际上只增加了 1。
前面提到,setState()
会延迟、批量执行。在一个周期的批量执行过程中,当 setState()
第一个参数为一个对象时,并不是依次执行队列中的 setState()
调用,而是先将对象浅合并(shallow merge),再执行更新。
Object.assign(
previousState,
{clicked: state.clicked + 1},
{clicked: state.clicked + 1},
...
)
所以,上面的演示中,每次点击计数只增加了 1。
函数式 setState
setState(updater[, callback])
前面提到,setState()
的 updater
参数我们比较熟悉的形式是对象,言外之意,还有我们不太熟悉的形式——函数:
this.setState((prevState, props) => stateChange[, callback]);
该函数接收两个参数,分别是组件的上一个状态 prevState
和当前属性 props
。这两个参数的值都能保证是最新的。该函数需要返回一个对象,对象的值为要修改的状态,React 会将返回的对象与当前状态浅合并,然后执行更新。如果不需要更新,可以返回 null
。
这就是所谓的「函数式 setState」。函数式 setState 会按照调用顺序加入队列,然后依次执行更新,每次 setState()
的 updater 函数的参数拿到的状态都是最新的,可以用于某些有状态依赖或者需要按一定顺序更新的场景。
调整一下刚才的演示代码:
// ...
increaseClickTimes = (id) => {
this.setState({
- clicked: this.state.clicked + 1,
+ return {
+ clicked: prevState.clicked + 1,
+ }
}, () => {
console.log(`click ${id}: clicked value -> ${this.state.clicked}`);
});
};
// ...
See the Pen React State: functional setState by hegfirose (@minwe) on CodePen.
如你所愿,每次点击都会增加 3 次计数。
不知你是否留意到 callback 中的日志:
click 1: clicked value -> 3
click 2: clicked value -> 3
click 3: clicked value -> 3
也就是说,一个周期内的函数式 setState,其执行顺序是:
updater1()
updater2()
updater3()
callback1()
callback2()
callback3()
而不是:
updater1()
callback1()
updater2()
callback2()
updater3()
callback3()
借助函数式 setState,我们可以把状态更新逻辑拆分到组件外部独立文件中,方便维护、复用:
// 组件外部
function increaseTime(state, props) {
return { clicked : state.clicked + 1 };
}
// 组件内部引用
class StateMergeDemo {
// ...
// inside your component class
increaseClickTimes () {
this.setState(increaseTime)
}
// ...
}
是不是看着有点面熟,有点 Redux 的感觉。
小结
本文梳理了 setState()
的用法以及其使用时容易踩坑的点,至于 setState()
背后更底层的实现,后续会再整理。
参考链接
- Functional setState is the future of React
- React.Component - setState
- React FAQ - Component State
- React - State and Lifecycle
延伸阅读: