Apollo Client乐观响应仅生效一次问题排查求助
问题场景
使用Apollo的**乐观响应(optimistic response)**结合cache.modify()实现列表即时添加新对象的UI效果,首次操作可正常即时更新,但后续每次突变都无法即时刷新UI(需等待3~5秒)。控制台显示缓存已即时更新,但UI未同步,仅首次突变有效。
数据读取代码
const { status } = useSession(); const { data: { deliveries } = { deliveries: null } } = useQuery( GET_DELIVERIES, { skip: status !== "authenticated", fetchPolicy: "cache-and-network" } ); useEffect(() => { console.log("Deliveries updated", deliveries, Date.now()); }, [deliveries]);
突变代码
addDelivery({ variables: { location: locationObj, offset }, optimisticResponse: { addDelivery: { __typename: "Delivery", id: `${Math.round(Math.random() * 999999)}`, createdAt: Date.now(), expectedEnd: null, routeId: null, location: { id: `${Math.round(Math.random() * 999999)}`, __typename: "GeoJSON", type: "Feature", geometry: { __typename: "Geometry", ...locationObj.geometry, }, properties: { __typename: "Properties", ...locationObj.properties, }, }, }, }, update(cache, { data: { addDelivery } }) { cache.modify({ fields: { deliveries(existingDeliveries) { console.log("Modifying cache.", existingDeliveries, addDelivery, Date.now()); const newDelivery = cache.writeFragment({ data: addDelivery, fragment: gql(` fragment NewDelivery on Delivery { id location { id type geometry { type coordinates } properties { gid name street housenumber } } createdAt expectedEnd routeId } `), }); return [...existingDeliveries, newDelivery]; }, }, }); }, });
Apollo Client配置
const cache = new InMemoryCache({ typePolicies: { Query: { fields: { deliveries: { merge(existing = [], incoming) { return incoming; }, }, }, }, Geometry: { keyFields: false, }, Properties: { keyFields: false, }, GeoJSON: { fields: { properties: { merge(existing = [], incoming) { return incoming; }, }, geometry: { merge(existing = [], incoming) { return incoming; }, }, }, }, }, }); const client = new ApolloClient({ link: splitLink, cache, connectToDevTools: true, });
核心原因
缓存合并策略错误
当前Query.deliveries的merge策略直接返回incoming(接口返回的完整列表),会覆盖缓存中已有的列表(包括乐观更新添加的临时对象)。首次乐观更新后,网络请求返回的完整列表覆盖缓存,后续乐观更新时,existingDeliveries是接口返回的列表,但乐观临时ID和真实ID不匹配,缓存合并时无法正确保留乐观数据,导致UI未感知到变化。cache.writeFragment的冗余使用
乐观响应阶段写入的临时ID对象,在真实数据返回后会被替换,但update函数中用cache.writeFragment写入的对象引用未及时更新,导致列表中的引用还是临时ID的对象,UI无法触发重渲染。GeoJSON字段的merge策略类型错误GeoJSON的properties和geometry是对象类型,但配置中写成了数组合并,导致缓存处理时出现异常,间接影响UI更新。
解决方法
1. 修正缓存合并策略
调整Query.deliveries的merge逻辑为增量合并,保留现有数据并去重;同时修正GeoJSON字段的merge策略为对象合并:
const cache = new InMemoryCache({ typePolicies: { Query: { fields: { deliveries: { merge(existing = [], incoming) { // 基于ID去重,合并现有数据与新数据 const merged = [...existing]; incoming.forEach(item => { if (!merged.find(d => d.id === item.id)) { merged.push(item); } }); return merged; }, }, }, }, Geometry: { keyFields: false, }, Properties: { keyFields: false, }, GeoJSON: { fields: { properties: { merge(existing = {}, incoming) { return incoming; }, }, geometry: { merge(existing = {}, incoming) { return incoming; }, }, }, }, }, });
2. 优化乐观响应与update函数逻辑
给临时ID添加前缀区分真实ID,去掉冗余的cache.writeFragment,直接合并数据并去重:
addDelivery({ variables: { location: locationObj, offset }, optimisticResponse: { addDelivery: { __typename: "Delivery", id: `temp-${Math.round(Math.random() * 999999)}`, // 临时ID加前缀 createdAt: Date.now(), expectedEnd: null, routeId: null, location: { id: `temp-loc-${Math.round(Math.random() * 999999)}`, // 临时location ID加前缀 __typename: "GeoJSON", type: "Feature", geometry: { __typename: "Geometry", ...locationObj.geometry, }, properties: { __typename: "Properties", ...locationObj.properties, }, }, }, }, update(cache, { data: { addDelivery } }) { cache.modify({ fields: { deliveries(existingDeliveries = []) { // 去重后返回新数组,触发UI重渲染 const hasDuplicate = existingDeliveries.some(d => d.id === addDelivery.id); return hasDuplicate ? existingDeliveries : [...existingDeliveries, addDelivery]; }, }, }); }, });
3. 调整查询的fetchPolicy(可选)
若无需每次从网络拉取完整列表,可将fetchPolicy改为"cache-first",减少网络请求对缓存的覆盖;若必须使用"cache-and-network",确保merge策略正确合并数据而非覆盖。
内容的提问来源于stack exchange,提问作者TheBrenolibre

