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

并行Fetch捕获组件状态引发竞态条件,求惯用解决方案

并行Promise实时更新React状态的问题

需求

希望并行触发多个基于Fetch的Promise,每个Promise resolve时立即处理返回值并追加到组件状态items中,即每次Promise完成都单独更新状态,而非等待所有Promise结束后一次性更新。

问题

如果给每个Promise单独添加.then()处理返回值,items状态会被每个闭包捕获。第一个处理函数调用setState时用的是[...items, firstResultItem],第二个用的是[...items, secondResultItem]——每个闭包无法感知其他处理函数对items的修改,最终会出现“最晚请求覆盖之前结果”的竞态条件,不符合预期。

使用Promise.all(...)的单个处理函数能避免竞态,但这会导致所有结果一次性更新,违背了实时更新的需求。

思路

曾考虑结合useRef与useState,或利用useEffect,但认为这是典型问题,应该有更简洁的惯用解决方案。

代码示例
  • 用imitateFetch(promiseResolutionValue, delayInMs)模拟Fetch请求
  • items默认包含一个条目,所有imitateFetch生成的新条目需要追加到当前items状态中
import { useState } from "react";

export interface Item {
  id: string;
  text: string;
}

export function App() {
  const [items, setItems] = useState<Item[]>([
    { id: new Date().toISOString(), text: new Date().toLocaleTimeString() },
  ]);

  const naiveUpdateItemListWith = (newItemTexts: string[]) => {
    const newItems = newItemTexts.map(newItemText => {
      const item: Item = {
        id: new Date().toISOString(),
        text: newItemText,
      };
      return item;
    });
    // 这里使用闭包捕获的旧items,导致竞态
    setItems([...items, ...newItems]);
  };

  const handleTriggerFetch = () => {
    // 有问题的写法:每个then里的更新依赖闭包中的旧状态
    const one = imitateFetch('one', 1000).then(text => naiveUpdateItemListWith([text]));
    const two = imitateFetch('two', 1500).then(text => naiveUpdateItemListWith([text]));
    const three = imitateFetch('three', 2000).then(text => naiveUpdateItemListWith([text]));
    Promise.all([one, two, three]);

    // 可行但不符合实时更新需求的写法:所有请求完成后一次性更新
    // const one = imitateFetch('one', 1000);
    // const two = imitateFetch('two', 1500);
    // const three = imitateFetch('three', 2000);
    // Promise.all([one, two, three]).then(texts => naiveUpdateItemListWith(texts));
  };

  return (
    <div>
      {items.map(item => <div key={item.id}>[{item.id}] -- {item.text}</div>)}

      <button onClick={handleTriggerFetch}>
        模拟并行请求
      </button>
    </div>
  );
} 

function imitateFetch<T>(valueToResolve: T, ms = 1000): Promise<T> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(valueToResolve);
    }, ms);
  });
}
解决方案

核心是使用React状态的函数式更新,让每次状态更新都基于最新的状态快照,而非闭包捕获的旧值。

修改naiveUpdateItemListWith函数,将setItems的参数改为函数形式:

const updateItemListWith = (newItemTexts: string[]) => {
  const newItems = newItemTexts.map(newItemText => {
    const item: Item = {
      id: new Date().toISOString(),
      text: newItemText,
    };
    return item;
  });
  // 函数式更新:prevItems是当前最新的状态
  setItems(prevItems => [...prevItems, ...newItems]);
};

然后在handleTriggerFetch中使用这个修改后的函数:

const handleTriggerFetch = () => {
  imitateFetch('one', 1000).then(text => updateItemListWith([text]));
  imitateFetch('two', 1500).then(text => updateItemListWith([text]));
  imitateFetch('three', 2000).then(text => updateItemListWith([text]));
};

这样每个Promise resolve时,都会基于当前最新的items状态追加新条目,彻底避免竞态条件,同时满足“实时更新”的需求。

原理

React的setState支持传入函数,函数的参数是当前最新的状态值(prevState),返回值是新的状态。这种方式不会受闭包捕获旧值的影响,因为每次调用时都会获取到最新的状态快照,确保更新逻辑的正确性。

内容的提问来源于stack exchange,提问作者Igor Soloydenko

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 06:06:45