一. 概述
我们知道React支持两种形式来创建组件,一种是 class 组件,一种是函数组件。在 React16.8 推出之前,如果要为组件添加状态,那么只能使用 class 组件来实现,并通过 setState 来修改状态值,达到更新组件的目的。在 React16.8 推出 Hook 后,可以利用 useState 来为函数组件添加状态,这让我们的状态管理更加容易。
前端开发的核心在于状态管理,在实际项目开发中,特别是对新手React开发人员而言,对setState和useState Hook 的使用上仍有一些容易犯的错误和注意事项,在本文中,我们将探讨如何避免这些问题。
二. Class 组件中的 setState 的使用
在 class 组件中添加一个构造函数constructor,然后在该函数中为 this.state赋初值添加状态.
1  | class ClassComponent extends Component {  | 
                                                                
通过this.setState({count:this.state.count + 1})改变 state 里面 count 的值。这样操作是可以达到预期效果,但是再次添加一行同样的代码看会发生什么
1  | addCount(params) {  | 
                                                                 
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  | addCount(params) {  | 

[callback]是setState()的第二个参数,为可选的回调函数,它将在setState完成合并并重新渲染组件后执行。因此我们也可以在这个回调函数中获取到最新的state值.
对State赋值
State里面的状态数据可能是值类型或是引用类型,对于基本数据类型(String(字符串),Number(数值),Boolean(布尔值),Undefined,Null)可以直接对其赋值。对于引用类型的数据(Array,Object)赋值时要特别注意:不要直接修改原始引用类型的值。例如:为数组增加一项
1  | // 错误,不要直接修改this.state的值  | 
正确的做法是使用concat或是ES6的扩展语法:
1  | // 正确  | 
注意:在修改数组类型的状态时不要使用
push、pop、shift、unshift等方法,因为这些方法都是在原数组的基础上修改,取而代之的是使用concat、slice、splice、filter或是ES6的扩展语法,返回一个新的数组。
对于对象类型的状态而言可以这样赋值:
1  | // 使用ES6 的Object.assgin方法  | 
三. 函数组件中的useState
Hook 是 React 16.8 的新增特性,它可以让你在不编写 class 组件的情况下使用 state 以及其他的 React 特性。useState 是允许你在 React 函数组件中添加 state 的 Hook.
1  | import React, { memo, useState } from 'react';  | 
更新state
useState的唯一参数是初始 state,返回值为:当前 state 以及更新 state 的函数。如果我们也像在 class 组件中使用对象对其进行赋值,并调用更新 state 函数,那么一定要注意:class 组件中的 this.setState是合并state,而函数组件中更新 state 的函数是替换当前的 state。例如要对count执行 +1 操作,以下写法是错误的:
1  | // 错误  | 

otherCount 的值变为了 undefined.
正确写法:
1  | setData(Object.assign({},{...data,count:data.count+1}))  | 
上面的写法这和预期的一样。但是,直接更新状态是一种不好的做法, 同样如果连续调用两次,count的值并没有像预期的那样改变.
1  | setData(Object.assign({},{...data,count:data.count+1}))  | 

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

获取State的最新值
我们知道State 的更新可能是异步的,但有时候的业务场景需要我们同步拿到变量的最新变化值,以便做下一步操作,可以用一下几种方式获取:
1. 回调函数内获取
在setXXX中使用回调函数更新
1  | setData(data => {  | 
2. 直接使用useEffect 获取
1  | ...  | 
3.使用Promise实现
1  | new Promise((resolve) => {  | 
四. 小结
需要注意的是,在 React18 之前,在非 React 调度流程如setTimeout setInterval ,直接在 DOM 上绑定原生事件等,setState 是同步的,除了这些,setState都是异步执行。从react18开始, 使用了createRoot创建应用后, 所有的更新都会自动进行批处理,也就是异步合并。后续文章中我们在探讨 React 的自动批处理流程,本文不再涉及。
一些参考:
State 和生命周期指南
深入学习:何时以及为什么 setState() 会批量执行?
深入:为什么不直接更新 this.state?