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

React自定义usePromise Hook添加清理回调规避竞态条件问题

问题根因

原实现存在三个核心问题,导致加清理逻辑后首次加载卡死、竞态条件无法规避:

  • 直接原地修改state中的Map缓存:cache.set(label, true)没有通过setCache做不可变更新,React严格模式下useEffect会双调用验证逻辑,第一次执行时直接修改了缓存对象,清理后第二次执行effect会直接命中缓存判断提前返回,loading状态永远不会被重置为false。
  • 依赖项包含不稳定的回调引用:调用方传入的内联箭头函数每次渲染都会生成新引用,加上缓存的非法修改,极易触发effect意外重跑或提前终止。
  • 没有请求有效性校验:旧请求返回后会无差别覆盖最新状态,无法解决竞态问题。
修复后完整实现

原有调用方式完全兼容,无需修改业务侧代码:

import { useState, useEffect, useRef } from 'react';

export const usePromise = (label, callbackThatMakesAPromise) => {
  const [cache, setCache] = useState(new Map());
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  // 存储最新请求回调,避免内联函数导致effect依赖不稳定
  const latestCallbackRef = useRef(callbackThatMakesAPromise);
  latestCallbackRef.current = callbackThatMakesAPromise;
  // 竞态标记:标识当前effect绑定的请求是否有效
  const activeRequestRef = useRef(false);

  useEffect(() => {
    // 命中缓存直接终止,不发起重复请求
    if (cache.has(label)) {
      setIsLoading(false);
      return;
    }

    // 标记当前请求为有效状态
    activeRequestRef.current = true;
    setIsLoading(true);
    // 不可变更新缓存标记,避免重复发起相同请求
    setCache(prevCache => new Map(prevCache).set(label, true));
    console.log(`usePromise is loading ${label}`);

    latestCallbackRef.current()
      .then((res) => {
        // 仅当前请求有效时才更新数据
        if (activeRequestRef.current) {
          setData(res);
          setError(null);
        }
      })
      .catch((err) => {
        if (activeRequestRef.current) {
          setError(err);
        }
      })
      .finally(() => {
        if (activeRequestRef.current) {
          setIsLoading(false);
        }
      });

    // 清理函数:effect重跑/组件卸载时标记当前请求失效
    return () => {
      activeRequestRef.current = false;
    };
  }, [label, cache]);

  return {
    data,
    error,
    isError: !!error,
    isLoading,
    refresh: () => {
      setCache(prevCache => {
        const updatedCache = new Map(prevCache);
        updatedCache.delete(label);
        return updatedCache;
      });
    },
  };
};
核心修改说明
  • 用ref存储易变值:latestCallbackRef始终保存最新的请求回调,将回调从effect依赖项中移除,彻底解决内联函数导致的effect重复触发问题,同时保证请求逻辑始终能拿到最新的闭包变量;activeRequestRef标记当前effect绑定的请求是否有效,不触发额外重渲染。
  • 实现标准竞态规避逻辑:每次effect执行时将请求标记置为有效,返回的清理函数(effect重跑、组件卸载时触发)将标记置为无效;所有请求结果的状态更新前都先校验标记,失效请求的返回结果直接丢弃,不会触发任何状态更新。
  • 修正缓存更新逻辑:所有缓存修改都通过setCache做不可变更新,发起请求前就标记对应label的缓存位避免重复请求,同时完全兼容原有refresh逻辑——refresh删除对应label缓存后会自动触发effect重新发起请求。
  • 补全状态更新边界:只有请求有效时才更新data、error、isLoading状态,避免无效更新导致的渲染异常。

内容的提问来源于stack exchange,提问作者Neil M

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 12:39:15