React 18异步代码下useState多状态更新的困惑
React 18 + TypeScript 状态更新不符合预期问题分析
代码示例
interface User { id: number; name: string; } function App() { const [users, setUsers] = useState<User[]>([]); const [error, setError] = useState(""); useEffect(() => { axios .get<User[]>("https://jsonplaceholder.typicode.com/users") .then((res) => setUsers(res.data)); }, []); const addUser = () => { const newUser = { id: 0, name: "Test" }; setUsers([newUser, ...users]); axios .post("https://jsonplaceholder.typicode.com/users", newUser) .then((res) => { console.log("size", users.length); setUsers([res.data, ...users]); }) .catch((err) => setError(err.message)); }; return ( <> {error && <p>{error}</p>} <button onClick={addUser}>Add</button> <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> </> ); } export default App;
问题描述
原本预期点击一次Add按钮会触发两次重渲染:
- 第一次在列表开头添加id为0的Test用户并渲染;
- axios.post请求完成后,第二次重渲染在列表开头添加服务器返回的id为11的Test用户,最终列表会出现两个重复的Test用户。
虽然知道React的状态批处理机制,但认为这两次更新属于不同批次,应该触发两次重渲染。但实际测试后,列表仅显示一个新增的Test用户,想理解哪里出现了偏差。
问题原因
核心问题是闭包导致的状态值捕获:
addUser函数在组件某次渲染时创建,函数内部的users变量捕获的是该渲染周期的状态值(也就是初始的10个用户数组)。- 第一次调用
setUsers([newUser, ...users])时,确实会把id为0的Test加到列表开头,触发重渲染。但此时axios.post的回调函数已经创建完成,它内部引用的users还是更新前的旧数组。 - 当请求完成后执行
setUsers([res.data, ...users]),这里的users依然是初始的10个用户数组,所以这次更新会直接把id为11的Test加到旧数组前面,完全覆盖了第一次更新的结果。 - 最终组件状态是
[id11的Test, ...初始10个用户],所以只会显示一个新增的Test用户。
解决方法
使用React的函数式状态更新,它会接收当前最新的状态值作为参数,避免闭包陷阱:
const addUser = () => { const newUser = { id: 0, name: "Test" }; setUsers([newUser, ...users]); axios .post("https://jsonplaceholder.typicode.com/users", newUser) .then((res) => { // 用函数式更新,拿到最新的prevUsers状态 setUsers(prevUsers => [res.data, ...prevUsers]); }) .catch((err) => setError(err.message)); };
这样处理后,请求完成时的更新会基于第一次更新后的最新数组,最终列表会显示[id11的Test, id0的Test, ...初始10个用户],符合预期。
内容的提问来源于stack exchange,提问作者nkthuan
相关产品推荐
相关产品推荐

