Next.js 14.2.3(页面路由)getServerSideProps+React Query缓存失效问题
问题分析与解决方案
核心问题根源
你遇到的重复请求问题,本质是每次getServerSideProps执行时都新建了QueryClient实例。React Query的缓存是绑定在单个QueryClient实例上的,每次页面路由切换触发getServerSideProps时,新的QueryClient完全没有之前的缓存数据,自然会重复发起API请求,哪怕你设置了staleTime和gcTime也无效。
解决方案:服务端共享缓存
要实现服务端缓存,需要让不同请求的QueryClient复用同一缓存源,有两种可行方案:
方案1:使用全局QueryClient实例(单实例部署场景)
在服务端创建一个全局的QueryClient,所有getServerSideProps请求复用该实例,利用其内置缓存避免重复请求。
// 仅在服务端运行的全局QueryClient实例 let serverQueryClient = null; function getServerQueryClient() { if (!serverQueryClient) { serverQueryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5分钟新鲜期 gcTime: 5 * 60 * 1000, // 5分钟缓存回收时间 cacheTime: 5 * 60 * 1000, }, }, }); } return serverQueryClient; } export const getServerSideProps: GetServerSideProps = async (context) => { const { provider_name } = context.params as Params; const queryClient = getServerQueryClient(); // 复用全局实例 // 先检查缓存,不存在才发起请求 let providerInfo = queryClient.getQueryData(providerInfoQueryKey(provider_name)); if (!providerInfo) { providerInfo = await queryClient.fetchQuery({ queryKey: providerInfoQueryKey(provider_name), queryFn: () => apiGetProviderInfo(provider_name), }); } const providerId = providerInfo.data.id; const prefetches = []; const queryConfigs = [ { queryKey: providerNoteQueryKey({ providerId }), queryFn: () => apiGetProviderNotes(providerId) }, { queryKey: hotProductsQueryKey({ providerId }), queryFn: () => apiGetHotProducts(providerId) }, { queryKey: providerWorkingHoursQueryKey({ providerId }), queryFn: () => apiGetWorkingHours(providerId) }, { queryKey: menuSettingsQueryKey({ providerId }), queryFn: () => apiGetMenuSettings(providerId) }, { queryKey: providerThemeQueryKey(providerId), queryFn: () => apiGetProviderTheme(providerId) }, { queryKey: menuListQueryKey({ providerId }), queryFn: () => apiGetMenuList(providerId) }, ]; // 批量检查缓存,仅无缓存时发起预取 for (const config of queryConfigs) { if (!queryClient.getQueryData(config.queryKey)) { prefetches.push(queryClient.prefetchQuery(config)); } } await Promise.all(prefetches); return { props: { dehydratedState: dehydrate(queryClient), }, }; };
方案2:使用外部分布式缓存(多实例部署场景)
如果你的应用是多实例集群部署(如Docker、K8s),全局QueryClient的缓存无法跨实例共享,此时需要用Redis等分布式缓存存储查询结果:
import LRU from 'lru-cache'; // 生产环境替换为Redis客户端 const cache = new LRU({ max: 100, ttl: 5 * 60 * 1000 }); export const getServerSideProps: GetServerSideProps = async (context) => { const { provider_name } = context.params as Params; const queryClient = new QueryClient(); // 先查外部缓存 let providerInfo = cache.get(providerInfoQueryKey(provider_name)); if (!providerInfo) { providerInfo = await apiGetProviderInfo(provider_name); cache.set(providerInfoQueryKey(provider_name), providerInfo); } // 将缓存数据同步到QueryClient用于客户端脱水 queryClient.setQueryData(providerInfoQueryKey(provider_name), providerInfo); const providerId = providerInfo.data.id; const queryConfigs = [ { queryKey: providerNoteQueryKey({ providerId }), queryFn: () => apiGetProviderNotes(providerId) }, { queryKey: hotProductsQueryKey({ providerId }), queryFn: () => apiGetHotProducts(providerId) }, { queryKey: providerWorkingHoursQueryKey({ providerId }), queryFn: () => apiGetWorkingHours(providerId) }, { queryKey: menuSettingsQueryKey({ providerId }), queryFn: () => apiGetMenuSettings(providerId) }, { queryKey: providerThemeQueryKey(providerId), queryFn: () => apiGetProviderTheme(providerId) }, { queryKey: menuListQueryKey({ providerId }), queryFn: () => apiGetMenuList(providerId) }, ]; // 批量处理缓存与同步 for (const config of queryConfigs) { let data = cache.get(config.queryKey); if (!data) { data = await config.queryFn(); cache.set(config.queryKey, data); } queryClient.setQueryData(config.queryKey, data); } return { props: { dehydratedState: dehydrate(queryClient), }, }; };
客户端补充配置
确保_app.tsx中正确处理脱水状态,避免客户端重复请求:
import { QueryClient, QueryClientProvider, Hydrate } from '@tanstack/react-query'; import { AppProps } from 'next/app'; function MyApp({ Component, pageProps }: AppProps) { const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, gcTime: 5 * 60 * 1000, }, }, }); return ( <QueryClientProvider client={queryClient}> <Hydrate state={pageProps.dehydratedState}> <Component {...pageProps} /> </Hydrate> </QueryClientProvider> ); } export default MyApp;
内容的提问来源于stack exchange,提问作者shahin behzadrad
相关产品推荐
相关产品推荐

