React useState理解及使用

前情提要:最近在写一个功能的时候,犯了一个关于 React Hooks 的低级错误。 目前项目的React版本还是16.12.0

最近在开发一个功能,比较简单,就是请求一个接口返回数据,渲染一个列表,在这个过程有一些 loading(加载中),finish(加载结束),empty(接口没有返回数据) 等状态来控制显示一些交互UI。

最开始代码大概如下:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import {getListData} from "@/api/list"


const List = () => {
const [list,setList] = useState([])
const [loading,setLoading] = useState(false)
const [finish,setFinish] = useState(false)
const [empty,setEmpty] = useState(false)
const [page,setPage] = useState(1)

const fetchData = ()=> {
setLoading(true)
getListData({page}).then(res=>{
if(res.code == 200 && res.data){
setList(res.data)
if(page == 1){
setEmpty(!res.data.length)
}
}else{
setList([])
}

}).finally(()=> setLoading(false))

}

return (
<div>
{loading
? <div>loading....</div>
: <>
{
list.map(ele=> <div key={ele.id}></div>)
}
</>
}
{
(finish && !empty) && <div>没有更多了</div>
}
{
empty && <div>暂无数据</div>
}
</div>
)

}

可以注意到,所有state是分离到多个了,

由于该页面有多个tab下,大体都是同样的list, 每个list 展示的 item 结构不一样,这样就会有 4 * 4 个 useState 分别控制每个list的data/loading 等状态。

于是我把每个list相关的数据给合并到一起了,于是就变成了:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

// const [list,setList] = useState([])
// const [loading,setLoading] = useState(false)
// const [finish,setFinish] = useState(false)
// const [empty,setEmpty] = useState(false)
// const [page,setPage] = useState(1)

const [listData,setListData]= useState({
list:[],
loading: false,
finish: false,
empty: false,
page: 1,
})

// const fetchData = ()=> {
// setLoading(true)
// getListData({page}).then(res=>{
// if(res.code == 200 && res.data){
// setList(res.data)
// if(page == 1){
// setEmpty(!res.data.length)
// }
// }else{
// setList([])
// }

// }).finally(()=> setLoading(false))

// }
const fetchData = ()=> {
setListData({...listData, loading:true})
getListData({page}).then(res=>{
if(res.code == 200 && res.data){
setListData({
...listData,
list:res.data,
empty: page == 1 &&!res.data.length
})
}else{
setListData({
...listData,
list:[],
})
}
}).finally(()=> {
setListData({
...listData,
loading: false
})
})
}

这个时候有的人就能看出问题来了,loading,empty,finish 的状态出现了问题,无法渲染出正确的结构。

这里涉及到 useState 异步属性,首先确认一点 useState是异步的。也就是说执行 setXXX 之后,并不能立刻更新state 的状态,

异步的 setXXX 优点就是用户体验和性能,在只有一个 useState的情况下,并不明显,若是有多个的情况,如果每更新一个 state 就去渲染一次页面,一方面是性能问题,另一方面如果页面结构比较复杂,就是耗费时间,影响用户体验,

而且setState 是分批执行的,即时同时有多个 setXXX 的话或者是同一个 setXXX 执行了多次 ,都会被放去一个队列,等下次渲染前合并更新。

尤其在上面的例子中,第一种不会出现问题,是因为每个状态是分离,各自的 setXXX 互不影响。

而在第二种中,每次设置新值是建立在旧值(上一次状态值)之上的,也就说现在如果按照 第二种方案,在 finally 中设置值的时候,无法获取到 then 执行到时候设置的值。但是 then 中执行 setListData 之后,并未更新 state 状态,到了 finally 的时候,拿到的 listData 还是旧的,那么 then中的 setListData 就相当于被 “覆盖” 了, 而我们预期的目的是要 “合并”

useState 提供了两种方式更新 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
const fetchData = ()=> {
setListData({...listData, loading:true})
getListData({page}).then(res=>{
if(res.code == 200 && res.data){
setListData((prev)=>(
{
...prev,
list:res.data,
empty: page == 1 &&!res.data.length
}
))
}else{
setListData((prev)=>(
{
...prev,
list:[],
}
))
}
}).finally(()=> {
setListData((prev)=>(
{
...prev,
loading: false
}
))
})
}

也可以从这个例子可以看出,setXXX 并不会立即去执行更新state,渲染结构,

虽然明知 useState 是异步,还是偶尔脑子抽抽,犯一些低级错误,

作者

Fat Dong

发布于

2023-01-04

更新于

2023-01-04

许可协议