setState与useState使用注意项

一. 概述

我们知道React支持两种形式来创建组件,一种是 class 组件,一种是函数组件。在 React16.8 推出之前,如果要为组件添加状态,那么只能使用 class 组件来实现,并通过 setState 来修改状态值,达到更新组件的目的。在 React16.8 推出 Hook 后,可以利用 useState 来为函数组件添加状态,这让我们的状态管理更加容易。

前端开发的核心在于状态管理,在实际项目开发中,特别是对新手React开发人员而言,对setStateuseState Hook 的使用上仍有一些容易犯的错误和注意事项,在本文中,我们将探讨如何避免这些问题。

二. Class 组件中的 setState 的使用

在 class 组件中添加一个构造函数constructor,然后在该函数中为 this.state赋初值添加状态.

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
class ClassComponent extends Component {

constructor(props) {
super(props)
this.state = {
count: 0,
otherCount: 100
}
this.addCount = this.addCount.bind(this);
}

addCount(params) {
this.setState({count:this.state.count + 1})
}

render() {
return (
<div>
class组件
<div>
count: {this.state.count}<br></br>
otherCount: {this.state.otherCount}
</div>
<div>
<button onClick={this.addCount}>+1</button>
</div>
</div>
);
}
}

export default ClassComponent;

pic01

通过this.setState({count:this.state.count + 1})改变 state 里面 count 的值。这样操作是可以达到预期效果,但是再次添加一行同样的代码看会发生什么

1
2
3
4
addCount(params) {
this.setState({count:this.state.count + 1})
this.setState({count:this.state.count + 1})
}

pic01

count 的值并没有像我们预想的那样每次点击+2。是什么原因导致的呢?
State 的更新可能是异步的,出于性能考虑,React 可能会把多个setState()调用合并成一个调用,要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。再来重新认识一下setState

setState()

1. setState(stateChange,[callback])

第一个参数是你要改变的 state 对象,第二个是可选的回调函数。这种形式的setState()是异步的,并且在同一周期内会对多个 setState进行批处理。后调用的 setState() 将覆盖同一周期内先调用 setState 的值,因此count数仅增加一次。如果要解决这个问题,我们需要为setState传入一个函数.

2. setState(updater,[callback])

第一个参数是带有形参的updater函数

1
(state, props) => stateChange

state对应变化时组件状态的引用,也就是最新的state,此次更新被应用时的props是第二个参数(可选)。因此可以使用这种方式来解决我们上面遇到的问题.

1
2
3
4
5
6
7
8
addCount(params) {
this.setState((state) => ({
count: state.count + 1
}))
this.setState((state) => ({
count: state.count + 1
}))
}

pic02

[callback]setState()的第二个参数,为可选的回调函数,它将在setState完成合并并重新渲染组件后执行。因此我们也可以在这个回调函数中获取到最新的state值.

对State赋值

State里面的状态数据可能是值类型或是引用类型,对于基本数据类型(String(字符串),Number(数值),Boolean(布尔值),Undefined,Null)可以直接对其赋值。对于引用类型的数据(Array,Object)赋值时要特别注意:不要直接修改原始引用类型的值。例如:为数组增加一项

1
2
3
4
5
6
7
8
// 错误,不要直接修改this.state的值
this.setState({
listData: this.state.listData.push(100)
})
// 错误
this.setState(preSate => ({
listData: preSate.listData.push(100)
}))

正确的做法是使用concat或是ES6的扩展语法:

1
2
3
4
5
6
7
8
// 正确
this.setState(preSate => ({
listData: preSate.listData.concat(100)
}))
// 正确
this.setState(preSate => ({
listData: [...preSate.listData,100]
}))

注意:在修改数组类型的状态时不要使用pushpopshiftunshift等方法,因为这些方法都是在原数组的基础上修改,取而代之的是使用concatslicesplicefilter或是ES6的扩展语法,返回一个新的数组。

对于对象类型的状态而言可以这样赋值:

1
2
3
4
5
6
7
8
// 使用ES6 的Object.assgin方法
this.setState(preState => ({
mapData: Object.assign({},preState.mapData,{name:'React'})
}))
// 使用对象扩展语法
this.setState(preState => ({
mapData: {...preState.mapData,name:'React'}
}))

三. 函数组件中的useState

Hook 是 React 16.8 的新增特性,它可以让你在不编写 class 组件的情况下使用 state 以及其他的 React 特性。useState 是允许你在 React 函数组件中添加 state 的 Hook.

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
import React, { memo, useState } from 'react';

const HookComponent = () => {
const [data,setData] = useState({
count:0,
otherCount: 100
})

const addCount = () => {
// wrong
// setState({count:state.count+1})
// correct
setData(Object.assign({},{...data,count:data.count+1}))
setData(Object.assign({},{...data,count:data.count+1}))
console.log('data.count=====',data.count); // 0
}

return (
<div>
函数组件
<div>
count: {data.count}<br></br>
otherCount: {data.otherCount}
</div>
<div>
<button onClick={addCount}>+1</button>
</div>
</div>
);
}
export default memo(HookComponent);

更新state

useState的唯一参数是初始 state,返回值为:当前 state 以及更新 state 的函数。如果我们也像在 class 组件中使用对象对其进行赋值,并调用更新 state 函数,那么一定要注意:class 组件中的 this.setState是合并state,而函数组件中更新 state 的函数是替换当前的 state。例如要对count执行 +1 操作,以下写法是错误的:

1
2
// 错误
setData({count:data.count+1})

pic04

otherCount 的值变为了 undefined.

正确写法:

1
setData(Object.assign({},{...data,count:data.count+1}))

上面的写法这和预期的一样。但是,直接更新状态是一种不好的做法, 同样如果连续调用两次,count的值并没有像预期的那样改变.

1
2
setData(Object.assign({},{...data,count:data.count+1}))
setData(Object.assign({},{...data,count:data.count+1}))

pic05

与 class 组件状态更新原理不同,这里setData()是替换当前的 state,只执行最后一次+1的操作。如果要解决这个问题需要传递给 setState() 一个回调函数,在这个回调函数中我们获取该实例的当前状态,例如 setState(currentState => currentState + newValue)

1
2
3
4
5
6
7
8
setData(data => ({
...data,
count:data.count + 1
}))
setData(data => ({
...data,
count:data.count + 1
}))

pic06

获取State的最新值

我们知道State 的更新可能是异步的,但有时候的业务场景需要我们同步拿到变量的最新变化值,以便做下一步操作,可以用一下几种方式获取:

1. 回调函数内获取

在setXXX中使用回调函数更新

1
2
3
4
5
6
7
8
setData(data => {
const count = data.count + 1
console.log('count====>>>',count); // 获取最新值,执行后续逻辑
return {
...data,
count:count
}
})
2. 直接使用useEffect 获取
1
2
3
4
5
6
7
8
9
... 
const addCount = () => {
setData(Object.assign({},{...data,count:data.count+1}))
}
// 使用 useEffect 解决数据同步问题
useEffect(() => {
// 获取最新值,执行后续逻辑
console.log('count====',data.count);
}, [data]);
3.使用Promise实现
1
2
3
4
5
6
7
8
9
10
 new Promise((resolve) => {
const newData = {
...data,
count:data.count + 1
}
setData(Object.assign({},newData))
resolve(newData)
}).then(data => {
console.log('data====',data); // 获取到最新值,进行后续逻辑处理
});

四. 小结

需要注意的是,在 React18 之前,在非 React 调度流程如setTimeout setInterval ,直接在 DOM 上绑定原生事件等,setState 是同步的,除了这些,setState都是异步执行。从react18开始, 使用了createRoot创建应用后, 所有的更新都会自动进行批处理,也就是异步合并。后续文章中我们在探讨 React 的自动批处理流程,本文不再涉及。

一些参考:

React

State 和生命周期指南
深入学习:何时以及为什么 setState() 会批量执行?
深入:为什么不直接更新 this.state?