使用useState Hook结合异步函数时为何出现并发问题?如何解决?
这是我亲身踩过、调研后解决的一个React状态更新问题,分享给大家避坑——当你使用useState Hook维护组件状态,同时在异步函数里通过setState({...state, updatedProperty: updatedValue})这种方式更新状态时,很容易遇到并发状态覆盖的问题,导致应用丢失数据。
问题场景
我当时在开发一个管理用户联系人的数据上下文,联系人数据存在云端MongoDB集群,由后端服务提供接口。在上下文提供者组件里,我用useState来维护状态:
const [state, setState] = useState({ contacts: [], selectedContact: null }); const setSelected = (contact) => setState({...state, selectedContact: contact}); const clearSelected = ()=> setState({...state, selectedContact: null}); const updateContact = async(selectedContact) => { const res = await /* 调用更新联系人的后端API */; const updatedContact = res.data; // 这里打印会发现,明明已经调用了clearSelected,state里的selectedContact还是旧值 console.log('当前状态:', JSON.stringify(state)); // 这个setState会把selectedContact重新设回旧值,覆盖之前的clearSelected操作 setState({ ...state, contacts: state.contacts.map(ct => ct._id === updatedContact._id ? updatedContact : ct) }); }
在编辑联系人的表单提交事件中,我按这个顺序调用函数:updateContact(); clearSelected();
问题现象
执行后会发现,selectedContact先是被clearSelected()设为null,但等updateContact里的API请求完成(异步等待结束)后,又被重置回之前选中的旧联系人值,导致界面状态异常。
解决方法
方法一:使用函数式更新
改用setState(prevState => ({...prevState, updatedProperty: updatedValue}))的函数式更新方式就能解决问题,因为函数式更新会接收最新的状态快照prevState作为参数,不会依赖异步函数闭包中保存的旧状态副本:
setState(prevState => ({ ...prevState, contacts: prevState.contacts.map(ct => ct._id === updatedContact._id ? updatedContact : ct) }));
修改后,updateContact里的setState会基于clearSelected更新后的最新状态来修改contacts,不会覆盖selectedContact为null的状态。
方法二:使用useReducer管理状态
如果你的状态逻辑比较复杂,也可以用useReducer来替代useState。用reducer管理状态时,即使采用常规的更新方式,也能避免这种并发覆盖问题,因为reducer会始终基于最新的状态来处理更新动作。
内容的提问来源于stack exchange,提问作者Mo Faried

