一. 概述
我们知道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?