You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 02:01:18