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
相关产品推荐
相关产品推荐

