React Hooks:在useEffect中避免并发GET请求并遵循依赖规则
问题:React Hooks中基于Redux状态变化发起请求,避免并发且遵循依赖规则
我正在开发React功能,需要在从Redux切片通过useSelector获取的mapState.bounds值每次变化时执行GET请求。当前简化实现如下:
const mapState = useSelector(selectMapState); const [searchFoo, { data, isFetching }] = useLazyGetFooSearchQuery(); const fetchData = React.useCallback(async (searchQuery: string) => { const { isError, error } = await searchFoo({ query: searchQuery }); if (isError) { console.error(error); } }, [searchFoo]); React.useEffect(() => { /* Abort if already fetching to prevent executing concurrent GET queries */ if (isFetching) return; const searchQuery = JSON.stringify({/* 基于mapState.bounds生成的查询参数 */ }); fetchData(searchQuery).catch((error) => console.error(error)); }, [ mapState.bounds, /* 加入这个会导致无限循环,但不加入违反React完整依赖数组准则 */ isFetching, fetchData, ]);
核心目标是避免mapState.bounds快速连续变化时执行并发GET请求。当前实现若将isFetching从useEffect依赖数组中移除可正常工作,但违反React的完整依赖数组准则;将其加入则会触发无限循环,流程如下:
mapState.bounds值变化useEffect触发- 异步调用
fetchData - Redux Toolkit将
isFetching设为true useEffect触发但立即返回data被填充,fetchData执行完毕isFetching变为falseuseEffect因上一步再次触发,回到步骤3
虽可在事件监听回调(如onBoundsChange)中执行请求,但我想了解如何使用React Hooks实现需求,同时保持hook中使用的所有变量在依赖数组内,符合官方推荐规范。请问如何借助React Hooks避免并发GET请求并遵循完整依赖规则?
解决方案
方法1:用AbortController取消旧请求,结合useRef跟踪请求状态
通过useRef跟踪活跃请求状态,同时用AbortController取消旧请求,既不需要将isFetching加入依赖,又能避免并发请求,完全遵循依赖规则:
const mapState = useSelector(selectMapState); const [searchFoo, { isError, error }] = useLazyGetFooSearchQuery(); // 跟踪当前请求的AbortController,用于取消旧请求 const abortControllerRef = React.useRef(null); // 跟踪是否有活跃请求,替代isFetching作为判断依据 const isRequestActiveRef = React.useRef(false); const fetchData = React.useCallback(async (searchQuery: string) => { // 取消未完成的旧请求 if (abortControllerRef.current) { abortControllerRef.current.abort(); } const controller = new AbortController(); abortControllerRef.current = controller; isRequestActiveRef.current = true; try { await searchFoo({ query: searchQuery, signal: controller.signal }); } catch (err) { // 忽略请求取消的错误 if (err.name !== 'AbortError') { console.error(err); } } finally { isRequestActiveRef.current = false; abortControllerRef.current = null; } }, [searchFoo]); React.useEffect(() => { // 仅当无活跃请求时发起新请求 if (isRequestActiveRef.current) return; const searchQuery = JSON.stringify({/* 基于mapState.bounds生成的查询参数 */ }); fetchData(searchQuery); // 组件卸载时取消请求 return () => { abortControllerRef.current?.abort(); }; }, [mapState.bounds, fetchData]);
方法2:用useRef保存历史bounds,过滤无效触发
保留isFetching在依赖数组中,但通过useRef记录上一次的bounds值,确保只有bounds真正变化时才执行请求逻辑,避免isFetching变化导致的无限循环:
const mapState = useSelector(selectMapState); const [searchFoo, { isFetching, isError, error }] = useLazyGetFooSearchQuery(); // 保存上一次的bounds值,用于判断是否真的发生变化 const prevBoundsRef = React.useRef(mapState.bounds); const fetchData = React.useCallback(async (searchQuery: string) => { const result = await searchFoo({ query: searchQuery }); if (result.isError) { console.error(result.error); } }, [searchFoo]); React.useEffect(() => { // 仅当bounds真正变化,且当前无请求时才发起 if (prevBoundsRef.current === mapState.bounds || isFetching) return; prevBoundsRef.current = mapState.bounds; const searchQuery = JSON.stringify({/* 基于mapState.bounds生成的查询参数 */ }); fetchData(searchQuery).catch(console.error); }, [mapState.bounds, isFetching, fetchData]);
方法3:防抖处理快速变化的bounds
如果bounds会快速连续变化(如地图拖拽场景),防抖可以延迟请求发起,确保只在用户停止操作后执行一次请求,同时结合AbortController取消中间无效请求:
// 可自行实现防抖函数,或引入lodash的debounce const debounce = (func, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }; const mapState = useSelector(selectMapState); const [searchFoo, { isError, error }] = useLazyGetFooSearchQuery(); const abortControllerRef = React.useRef(null); // 用useCallback包裹防抖函数,确保引用稳定 const debouncedFetch = React.useCallback(debounce(async (bounds) => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } const controller = new AbortController(); abortControllerRef.current = controller; try { const searchQuery = JSON.stringify({/* 基于bounds生成的查询参数 */ }); await searchFoo({ query: searchQuery, signal: controller.signal }); } catch (err) { if (err.name !== 'AbortError') { console.error(err); } } finally { abortControllerRef.current = null; } }, 300), [searchFoo]); React.useEffect(() => { debouncedFetch(mapState.bounds); return () => { debouncedFetch.cancel(); abortControllerRef.current?.abort(); }; }, [mapState.bounds, debouncedFetch]);
内容的提问来源于stack exchange,提问作者Tomasz Kasperczyk
相关产品推荐
相关产品推荐

