React useState数组在setTimeout后仍为空的原因咨询
问题:React异步函数中state未更新导致数据被覆盖
我正在学习React,使用useState() Hook维护任务数组,设置5秒延迟后通过异步fetch从远程接口添加数据。但在5秒内手动添加了Butter和Milk到购物列表后,fetchData函数中的tasks数组仍显示为空,导致原有数据被接口返回的数据覆盖。
相关代码如下:
const [tasks, setTasks] = useState([]); const url = 'https://pokeapi.co/api/v2/pokemon?offset=0&limit=5'; // asnynchronous call. Yes, it's possible to use axios as well const fetchData = async () => { try { let tasksArray = []; await fetch(url) .then((response) => response.json()) .then((data) => { data.results.map((task, index) => { // first letter uppercase const taskName = task.name.charAt(0).toUpperCase() + task.name.slice(1); tasksArray.push({ id: index, name: taskName }); }); }); console.log('Added tasks:' + tasks.length); setTasks(_.isEmpty(tasks) ? tasksArray : [...tasks, tasksArray]); } catch (error) { console.log('error', error); } }; useEffect(() => { // Add additional example tasks from api after 5 seconds with // fetch fetchData promise setTimeout(fetchData, 5000); }, []);
手动添加任务的handleSubmit代码:
const handleSubmit = (e) => { e.preventDefault(); //creating new task setTasks([ { id: [...tasks].length === 0 ? 0 : Math.max(...tasks.map((task) => task.id)) + 1, name: newTask, isFinished: false, }, ...tasks, ]); setNewTask(''); }
原因分析
这是闭包陷阱导致的,和useEffect执行时机、异步行为都有关:
- 组件挂载时,useEffect依赖数组为空,只执行一次,此时创建的
fetchData函数捕获了当时的tasks初始值(空数组)。 - 手动添加任务时,
tasks状态更新触发组件重新渲染,但fetchData还是挂载时的那个版本,里面引用的始终是初始的空数组,和最新状态完全无关。 - 5秒后
fetchData执行,读取的是闭包里保存的旧tasks值,所以控制台打印的长度为0,最终用接口数据覆盖了手动添加的内容。
另外代码还有个小bug:setTasks里的[...tasks, tasksArray]会把整个接口返回的数组作为单个元素插入,导致tasks变成嵌套数组,应该改成[...tasks, ...tasksArray]展开每个任务对象。
解决方法
1. 使用函数式更新(推荐)
调用setTasks时传入函数,函数会接收当前最新的state作为参数,这样就能获取到手动添加后的tasks数组:
const fetchData = async () => { try { let tasksArray = []; // 把then链式改成await,代码更清晰 const response = await fetch(url); const data = await response.json(); data.results.forEach((task, index) => { const taskName = task.name.charAt(0).toUpperCase() + task.name.slice(1); tasksArray.push({ id: index, name: taskName }); }); // 函数式更新,获取最新的prevTasks setTasks(prevTasks => { console.log('Added tasks:' + prevTasks.length); return [...prevTasks, ...tasksArray]; }); } catch (error) { console.log('error', error); } };
2. 优化handleSubmit的闭包问题
同样,handleSubmit里的id生成也可以用函数式更新,避免后续可能出现的闭包问题:
const handleSubmit = (e) => { e.preventDefault(); setTasks(prevTasks => { const newId = prevTasks.length === 0 ? 0 : Math.max(...prevTasks.map(task => task.id)) + 1; return [ { id: newId, name: newTask, isFinished: false }, ...prevTasks ]; }); setNewTask(''); }
注意:不要尝试给useEffect添加
tasks依赖,这会导致每次tasks更新都重新创建setTimeout,5秒计时会被重置,不符合“延迟5秒后添加接口数据”的需求。
内容的提问来源于stack exchange,提问作者Bambi Bunny
相关产品推荐
相关产品推荐

