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

React中如何避免依赖型API请求的竞态条件?

问题描述

使用React的react-select自定义组件时,API请求存在竞态条件,导致下拉选择器无法正确填充选中值。查阅多篇关于useEffect中处理fetch调用的Stack Overflow问题仍未解决,不确定是竞态条件还是fetch的Promise处理不当导致。

相关代码

AlergiaForm.component.tsx

import Select, { SingleValue} from "react-select";
import {useState, useEffect} from "react";
interface IOption {
  value: string | null;
  label: string;
  icon?: string;
}

interface IProduct { 
  id: number,
  title: string
}

const AlergiaForm = () => {
  const[products, setProducts] = useState(Array<IProduct>);
  const[product, setProduct] = useState<SingleValue<IOption> | null>(null);
  const[isLoading, setLoading] = useState(false);
  const[value, setValue] = useState<string | null | undefined>("");

  //Mapper
  const mapper = ():IOption[] => {
    return products.map(item => {
      return { value: item.id, label: item.title};
    }) as unknown as IOption[];
  }

   useEffect(() => {
    //Get product, like an edit
    const getProduct = () => {
      setLoading(true);
      fetch('https://dummyjson.com/products/6')
      .then(res => res.json())
      .then((data) => {
          //We get the products dropdown values and we are waiting for it... is not even async and I'm waiting on the promise I think?
          getProducts();

          setLoading(false);
          //Logic to "set" the current value, but since it has a race condition, it's not picking it up
          //Works when a hot module refresh happens in Vite because the context is updated already... but not on page "load" or react-router-dom navigation
          const product = data as IProduct;
          const {id} = {...product};
          const selectedProduct = products.find(i => i.id === id) as IProduct;
          const ioption = { value: selectedProduct?.id.toString(), label: selectedProduct?.title } as SingleValue<IOption>;
          setProduct(ioption);
      });
    }

    const getProducts = () => {
      fetch('https://dummyjson.com/products')
      .then(res => res.json())
      .then((data) => {
        const products = data.products as IProduct[];
        setProducts(products);
      });
    }             

    getProduct();

   }, []);

   const handleDropDownChange = (selectedOption: SingleValue<IOption>):void => {
    const { value } = {...selectedOption};
    setValue(value);
}

  return(
    <>
      <Select options={mapper()} value={isLoading ? null : product} onChange={handleDropDownChange} />
      <p>Value selected: {product?.value ?? value}</p>
      <p>Is Loading: {isLoading ? "Loading..." : "Done"}</p>
    </>
  )  
}

  export default AlergiaForm;

App.tsx

import AlergiaForm from './AlergiaForm.component'

function App() {
  return (
    <main>
      <AlergiaForm />
    </main>
  )
}

export default App;

问题详情

获取产品数据后无法更新选中值,推测是Promise未完成就尝试设置值导致;Vite热重载后值能正常选中,说明逻辑本身可行,问题出在竞态条件上。已阅读相关React useEffect数据获取的技术文章。


问题修复方案

问题根源

  1. 请求顺序未控制:调用getProducts()后立刻执行选中值设置逻辑,但getProducts是异步请求,此时products仍是初始空数组,无法找到对应产品,导致setProduct设置无效值。
  2. 初始状态错误:useState(Array<IProduct>)未初始化空数组,而是传入数组构造函数,导致初始products不是有效数组实例。
  3. 加载状态管理不当:setLoading(false)在单个产品请求完成后就设置,忽略了getProducts请求可能仍在进行的情况。

修改后的代码(AlergiaForm.component.tsx)

import Select, { SingleValue} from "react-select";
import {useState, useEffect} from "react";

interface IOption {
  value: string | null;
  label: string;
  icon?: string;
}

interface IProduct { 
  id: number,
  title: string
}

const AlergiaForm = () => {
  // 修正初始状态为有效空数组
  const[products, setProducts] = useState<IProduct[]>([]);
  const[product, setProduct] = useState<SingleValue<IOption> | null>(null);
  const[isLoading, setLoading] = useState(false);
  // 新增状态存储目标产品数据
  const[targetProduct, setTargetProduct] = useState<IProduct | null>(null);
  const[value, setValue] = useState<string | null | undefined>("");

  // 简化映射函数
  const mapper = ():IOption[] => {
    return products.map(item => ({
      value: item.id.toString(),
      label: item.title
    }));
  }

  useEffect(() => {
    setLoading(true);
    // 并行发起两个请求,确保所有数据就绪后再更新状态
    const fetchData = async () => {
      try {
        const [productRes, productsRes] = await Promise.all([
          fetch('https://dummyjson.com/products/6').then(res => res.json()),
          fetch('https://dummyjson.com/products').then(res => res.json())
        ]);
        
        setTargetProduct(productRes as IProduct);
        setProducts(productsRes.products as IProduct[]);
      } catch (err) {
        console.error("请求失败:", err);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  // 监听products和targetProduct变化,就绪后设置选中值
  useEffect(() => {
    if (products.length > 0 && targetProduct) {
      const selected = products.find(i => i.id === targetProduct.id);
      if (selected) {
        setProduct({
          value: selected.id.toString(),
          label: selected.title
        });
      }
    }
  }, [products, targetProduct]);

  // 简化下拉选择回调
  const handleDropDownChange = (selectedOption: SingleValue<IOption>):void => {
    setValue(selectedOption?.value);
    setProduct(selectedOption);
  }

  return(
    <>
      <Select 
        options={mapper()} 
        value={isLoading ? null : product} 
        onChange={handleDropDownChange} 
        isLoading={isLoading}
      />
      <p>Value selected: {product?.value ?? value}</p>
      <p>Is Loading: {isLoading ? "Loading..." : "Done"}</p>
    </>
  )  
}

export default AlergiaForm;

关键修改点

  1. 修正初始状态:将products初始值设为[]并明确类型,确保初始为有效数组。
  2. 并行请求+等待完成:用Promise.all同时发起两个请求,确保所有数据获取完成后再更新状态,彻底避免竞态。
  3. 分离状态监听:新增targetProduct存储单个产品数据,通过独立useEffect监听两个状态的变化,确保查找选中项时products已是最新数据。
  4. 优化加载状态:将setLoading(false)放在finally块中,确保请求无论成败都会结束加载,同时给Select组件添加isLoading属性提升体验。
  5. 简化代码逻辑:精简映射函数和回调逻辑,移除不必要的类型断言。

内容的提问来源于stack exchange,提问作者Jorge F

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 15:12:03