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数据获取的技术文章。
问题修复方案
问题根源
- 请求顺序未控制:调用
getProducts()后立刻执行选中值设置逻辑,但getProducts是异步请求,此时products仍是初始空数组,无法找到对应产品,导致setProduct设置无效值。 - 初始状态错误:
useState(Array<IProduct>)未初始化空数组,而是传入数组构造函数,导致初始products不是有效数组实例。 - 加载状态管理不当:
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;
关键修改点
- 修正初始状态:将
products初始值设为[]并明确类型,确保初始为有效数组。 - 并行请求+等待完成:用
Promise.all同时发起两个请求,确保所有数据获取完成后再更新状态,彻底避免竞态。 - 分离状态监听:新增
targetProduct存储单个产品数据,通过独立useEffect监听两个状态的变化,确保查找选中项时products已是最新数据。 - 优化加载状态:将
setLoading(false)放在finally块中,确保请求无论成败都会结束加载,同时给Select组件添加isLoading属性提升体验。 - 简化代码逻辑:精简映射函数和回调逻辑,移除不必要的类型断言。
内容的提问来源于stack exchange,提问作者Jorge F
相关产品推荐
相关产品推荐

