前情提要:最近在写一个功能的时候,犯了一个关于 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 [listData,setListData]= useState({ list:[], loading: false, finish: false, empty: false, page: 1, })
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
是异步,还是偶尔脑子抽抽,犯一些低级错误,