React中useMemo缓存Promise失效问题及useEffect异步优化咨询
React SPA内切换标签页避免重复API请求的解决方案
问题原因
你当前的代码中,useMemo的缓存仅在组件单次挂载周期内有效。当切换标签页导致组件卸载,再返回时组件会重新挂载,此时所有hooks(包括useMemo)都会重新初始化,因此loadCard会被再次调用,发起新的API请求。即使Promise本身resolve后再次await会返回结果,但组件重新挂载时useMemo会重新执行,生成新的Promise,最终导致重复请求。
解决方案
方案1:模块级变量缓存Promise
将Promise缓存到组件外部的模块级变量中,这样即使组件卸载再挂载,缓存的Promise依然存在,只有页面刷新时才会重置缓存。同时加入token校验,确保token变化时重新发起请求:
// 模块级缓存变量,页面刷新才会清空 let banksCachePromise = null; let lastUsedToken = null; function YourComponent() { const [state, setState] = useState({ banks: null }); const token = useToken(); // 假设这里获取用户token // 仅当token变化或无缓存时,重新发起请求 const banksPromise = useMemo(() => { if (!banksCachePromise || token !== lastUsedToken) { lastUsedToken = token; banksCachePromise = (async () => { const banks = await getBankInfo(token); return banks; })(); } return banksCachePromise; }, [token]); useEffect(() => { let isMounted = true; banksPromise .then(banks => { if (isMounted) { setState({ banks }); } }) .catch(err => { console.error('获取银行信息失败:', err); }); // 组件卸载时标记为已卸载,避免setState报错 return () => { isMounted = false; }; }, [banksPromise]); // 组件渲染逻辑 return ( <div> {state.banks ? ( <ul> {state.banks.map(bank => ( <li key={bank.id}>{bank.name}</li> ))} </ul> ) : ( <p>加载中...</p> )} </div> ); }
方案2:使用数据缓存库(推荐)
使用React Query、SWR这类专门的数据缓存库,它们内置了请求缓存、重复请求拦截、缓存失效控制等功能,无需手动管理Promise缓存,代码更简洁可靠。
以React Query为例:
import { useQuery } from '@tanstack/react-query'; function YourComponent() { const token = useToken(); // useQuery自动缓存请求结果,queryKey变化时重新请求 const { data: banks, isLoading, error } = useQuery({ queryKey: ['bankInfo', token], // 用token作为缓存键,token变化则重新请求 queryFn: () => getBankInfo(token), staleTime: Infinity, // 缓存永不过期,页面刷新才会重新请求 }); if (isLoading) return <p>加载中...</p>; if (error) return <p>加载失败:{error.message}</p>; return ( <ul> {banks.map(bank => ( <li key={bank.id}>{bank.name}</li> ))} </ul> ); }
额外问题:useEffect中使用异步函数的优化写法
你之前的变通写法是可行的,以下两种简化方式同样可靠,同时注意添加组件卸载后的状态判断,避免内存泄漏:
方式1:内部定义async函数并执行
useEffect(() => { const fetchData = async () => { try { const banks = await banksPromise; setState({ banks }); } catch (err) { console.error('请求失败:', err); } }; fetchData(); // 清理函数:组件卸载时可在此取消请求(若getBankInfo支持取消) return () => {}; }, [banksPromise]);
方式2:使用.then().catch()链式调用
useEffect(() => { let isMounted = true; banksPromise .then(banks => isMounted && setState({ banks })) .catch(err => console.error('请求失败:', err)); return () => { isMounted = false; }; }, [banksPromise]);
注意:不能直接将async函数作为useEffect的回调,因为async函数返回的是Promise,而useEffect期望返回的是清理函数(或undefined),直接返回Promise会触发React警告。
内容的提问来源于stack exchange,提问作者libik
相关产品推荐
相关产品推荐

