Apollo Client分页API缓存命中仍发请求,如何自定义cache-first策略?
问题
我采用Apollo Client核心分页API方案,通过merge函数累积分页请求数据,计划用read函数重新分页。目前功能正常,但存在问题:即使目标页面的所有内容已在缓存中,每次切换页面仍会发起请求,违背了重新分页的初衷。
我使用默认的fetchStrategy——cache-first,官方定义为:如果缓存中存在所有请求的数据,则直接返回;否则执行服务器查询并缓存结果。
我疑惑的是:在分页实现场景下,Apollo Client如何检查缓存中是否存在所有请求数据?当前即使keyArgs匹配且数据已在缓存中,仍会发起请求。请问是什么原因导致该问题?如何自定义cache-first策略,使其检查请求页面的所有条目是否已存在于缓存中?
参考代码
TypePolicy配置
typePolicies: { Query: { fields: { paginatedProductTracking: { // Include everything except 'skip' and 'take' to be able to use `fetchMore` // and repaginate when reading cache // (essential for switching between desktop pagination and mobile lazy loading // without having to refetch) keyArgs: (args) => JSON.stringify(omit(args, ['query.skip', 'query.take'])), merge: (existing, incoming, { args }) => { if (!existing) { return incoming; } if (!incoming) { return existing; } const data = existing.paginatedData; const newData = incoming.paginatedData; return { ...existing, // conservative merge that is robust against pages being requested out of order paginatedData: [ ...data.slice(0, args?.query.skip || 0), ...newData, ...data.slice((args?.query.skip || 0) + newData.length), ], }; }, }, }, }, },
组件内查询代码
const [pageSize, setPageSize] = useState(100); const [page, setPage] = useState(0); const skip = page * pageSize; const query = { filter, aggregationInterval, order, skip, take: pageSize, search: search ? values : null, locations: currentLocations.length > 0 ? currentLocations.map((location) => location.id) : undefined, }; const { data, loading, fetchMore } = useProductTrackingAggregatedDataQuery({ variables: { query, }, }); onPageChange={async (newPage) => { await fetchMore({ variables: { query: { ...query, skip: newPage * pageSize, }, }, }); setPage(newPage); }}
原因分析
- 缺少
read函数:当前typePolicies仅配置了merge,未实现read函数。Apollo Client默认缓存读取逻辑无法识别合并后的分页数据结构,无法判断目标页面内容是否已在缓存中。 cache-first默认检查逻辑:默认情况下,cache-first会检查缓存中是否存在与当前查询参数完全匹配的完整结果。但由于你将skip和take排除在keyArgs之外,缓存中不会存储每个页面的单独完整结果,而是存储合并后的数据集。此时Apollo无法自动从合并数据中提取目标页面内容,只能发起新请求。
解决方法
1. 实现read函数,从缓存提取目标页面数据
在paginatedProductTracking的字段配置中添加read函数,根据当前请求的skip和take参数,从合并后的paginatedData中截取对应页面内容,并判断缓存是否包含目标页面的所有条目,以此决定是否触发请求。
修改后的TypePolicy配置:
typePolicies: { Query: { fields: { paginatedProductTracking: { keyArgs: (args) => JSON.stringify(omit(args, ['query.skip', 'query.take'])), merge: (existing, incoming, { args }) => { if (!existing) { return incoming; } if (!incoming) { return existing; } const data = existing.paginatedData; const newData = incoming.paginatedData; return { ...existing, paginatedData: [ ...data.slice(0, args?.query.skip || 0), ...newData, ...data.slice((args?.query.skip || 0) + newData.length), ], }; }, read: (existing, { args }) => { if (!existing) return undefined; // 缓存无数据,触发请求 const { skip = 0, take = 100 } = args?.query || {}; const endIndex = skip + take; // 检查缓存数据是否覆盖目标页面所有条目 if (existing.paginatedData.length >= endIndex) { // 从缓存截取目标页面数据 return { ...existing, paginatedData: existing.paginatedData.slice(skip, endIndex), }; } // 缓存缺少目标页面部分数据,返回undefined触发请求 return undefined; }, }, }, }, },
2. 调整页面切换逻辑
通过上述read函数的实现,cache-first策略会自动处理请求:如果缓存中已有完整页面数据,直接返回;否则才发起请求。无需在onPageChange中强制调用fetchMore,可直接更新页面状态,由查询自动读取缓存或发起请求。
关键说明
read函数是核心:负责将缓存中合并的完整数据集转换为当前查询请求的分页结果,同时通过判断数据长度是否覆盖目标页面范围,决定是否触发请求。keyArgs配置保持不变:确保不同skip/take的请求共享同一个缓存条目,保证merge能正确累加数据。
内容的提问来源于stack exchange,提问作者Markus
相关产品推荐
相关产品推荐

