React中useEffect请求API时如何避免竞态条件?
解决React Hook中请求竞态条件的问题
你的问题出在running状态的闭包陷阱上:当getData异步执行时,它捕获的是函数创建时的running值,后续useEffect清理函数调用setRunning(false)更新的是新状态,但旧的getData里的running还是原来的true,导致判断逻辑无法阻止旧请求的响应更新状态。
下面提供两种可靠的解决方案:
方案一:使用AbortController取消请求(推荐)
这是最合理的方案,不仅能阻止旧请求的状态更新,还能直接取消未完成的HTTP请求,节省资源。现代浏览器和Axios(你的instance应该是Axios实例)都支持AbortController。
修改后的代码:
import { useEffect, useState } from "react"; import { instance } from "../src/App"; export const useInstance = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; const getData = async () => { setLoading(true); setError(null); // 切换URL时清空之前的错误 try { const response = await instance.get(url, { signal }); // 传入signal setData(response.data); setLoading(false); } catch (error) { // 忽略取消请求的错误 if (error.name !== 'CanceledError') { setError(error.message); setLoading(false); } } }; getData(); // 清理函数:取消当前请求 return () => controller.abort(); }, [url]); return [data, error, loading]; };
原理说明:
- 每次URL变化时,创建新的
AbortController,并将其signal传入Axios请求配置。 - 当组件卸载或URL切换时,执行清理函数调用
controller.abort(),Axios会立即取消请求,并抛出CanceledError,我们在catch中忽略这个错误,就不会更新状态了。
方案二:使用Ref跟踪请求有效性
如果你的请求库不支持AbortController,可以用React的useRef来跟踪当前请求是否有效,因为ref的更新是同步的,不会有闭包陷阱。
修改后的代码:
import { useEffect, useState, useRef } from "react"; import { instance } from "../src/App"; export const useInstance = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const isCurrentRequest = useRef(true); // 用ref跟踪当前请求是否有效 useEffect(() => { isCurrentRequest.current = true; // URL变化时标记当前请求有效 const getData = async () => { setLoading(true); setError(null); try { const response = await instance.get(url); // 只有当前请求是有效的,才更新状态 if (isCurrentRequest.current) { setData(response.data); setLoading(false); } } catch (error) { if (isCurrentRequest.current) { setError(error.message); setLoading(false); } } }; getData(); // 清理函数:标记当前请求无效 return () => { isCurrentRequest.current = false; }; }, [url]); return [data, error, loading]; };
原理说明:
useRef创建的isCurrentRequest在组件生命周期内保持同一个引用,更新其current属性是同步的,不会触发重新渲染。- 每次URL变化时,先将
isCurrentRequest.current设为true,表示当前请求是有效的。 - 当请求完成后,先检查
isCurrentRequest.current是否为true,只有是true时才更新状态。 - 清理函数将
isCurrentRequest.current设为false,旧请求完成时就会跳过状态更新。
内容的提问来源于stack exchange,提问作者Jerome
相关产品推荐
相关产品推荐

