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

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变为false
  • useEffect因上一步再次触发,回到步骤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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 13:44:52