Apollo Client 在 GraphQL 中的角色
Apollo Client 是面向 GraphQL 的客户端数据层:负责发送 operation、跟踪加载/错误状态,并围绕 GraphQL 实体维护 normalized cache、Mutation reconciliation 与实时更新。
[!info] related notes
- 所属 MOC: graphql-moc
- 前置概念: graphql、graphql-schema
- 并列概念: TanStack Query 服务端状态管理
- 易混淆概念: GraphQL Normalized Cache
- 关系笔记: GraphQL Subscriptions
Apollo Client 在 GraphQL 中的角色
一句话定义
Apollo Client 是一个理解 GraphQL operation 和实体关系的客户端数据层。
它不等于 GraphQL;GraphQL 是规范与执行模型,Apollo Client 是其中一种客户端实现。
它主要负责什么
在 React 中,一次查询大致经过:
React component
↓
Apollo Client
↓ GraphQL Query + variables
GraphQL Server
↓
data / errors
↓
Apollo Cache
↓
React re-render
它通常提供:
- Query / Mutation / Subscription 执行;
- loading / data / error 状态;
- variables 管理;
- normalized cache;
- mutation 后缓存 reconciliation;
- refetch 与 cache policy;
- Subscription payload 与缓存的整合。
与 TanStack Query 的关键区别
两者都能管理 server state,但抽象中心不同。
TanStack Query 主要认识:
queryKey → queryFn → cached result
例如:
['user', id]
Apollo Client 还理解 GraphQL 返回的实体身份与字段关系:
User:1
Post:10
Diagnosis:d9
同一个实体出现在多个 Query 中时,可以共享同一 normalized entity。
因此:
TanStack Query 更偏 query-centric cache;Apollo Client 更偏 entity/graph-centric cache。
这是心智模型上的核心差异,而不是谁能不能“直接改缓存”。
TanStack Query 也能直接修改缓存
不要把 TanStack Query 简化为:
Mutation
→ invalidate
→ refetch
它同样可以:
setQueryData
setQueriesData
optimistic update
mutation response → patch cache
invalidateQueries + refetch 常见,是因为当客户端不能可靠推导服务端最终状态时,重新取 authority 更稳妥;不是因为 TanStack Query 没有本地更新能力。
Query 变量
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
$id 属于 operation 的变量,而不是把字符串直接拼进 GraphQL 文本。
这使 operation 可以复用,也让参数经过 GraphQL 类型校验。
Mutation 后 Apollo 到底怎样自动更新
一个容易误解的说法是:
“Apollo 根据 Mutation 入参知道用户改了什么,所以自动改本地缓存。”
默认机制更准确的是:Apollo 主要根据 Mutation 的响应结果做 normalization 和 merge,而不是仅凭输入参数猜测服务端最终状态。
例如缓存已有:
User:42
name = "Sean"
Mutation:
mutation {
updateUser(id: "42", input: { name: "Arlon" }) {
id
name
}
}
服务端返回可识别实体:
__typename = User
id = 42
name = Arlon
Apollo 可以识别:
User:42
然后把 name merge 成 Arlon。所有引用 User:42 的查询视图都有机会同步更新。
如果 Mutation 只返回 success
updateUser(...) {
success
}
Apollo 并不能自动知道:
哪个 User 变了?
哪些字段变了?
哪些列表成员关系变了?
此时通常选择:
1. 让 Mutation 返回更新后的实体
2. cache.modify / writeFragment / update
3. refetchQueries
Optimistic Update 是另一回事
如果客户端希望点击后立即显示新值,可以用 mutation 输入构造 optimisticResponse 或手动 patch cache:
用户点击
→ 先乐观修改本地 projection
→ 服务端响应
→ 用真实结果校正
这是显式的客户端策略,不应和“Mutation 响应实体被 normalized merge”混成同一个机制。
本客户端写入与外部变化是两个问题
当前客户端自己发起 Mutation
如果当前页面自己执行:
Mutation
→ Server update
→ 返回更新后的 Entity
→ Apollo normalized merge
→ 当前 UI 更新
通常不需要为了这一次变化再建立 Subscription。
当前客户端没有发起变化
例如:
用户 A / 设备 A / 后台任务
→ 修改服务器状态
用户 B 当前页面
→ 没有发起这次 Mutation
用户 B 的 Apollo Cache 不会凭空知道服务器已经变化。
需要某种“变化通知”机制:
GraphQL Subscription
SSE / WebSocket event
Polling
focus/reconnect refetch
如果使用 GraphQL Subscription:
Server event
→ Subscription payload
→ identify Entity
→ normalized cache merge / explicit update
→ UI re-render
所以:
Mutation reconciliation 解决“我刚刚自己写了以后,本地怎么跟上”;Subscription 解决“别人或服务器改了以后,我怎么及时知道”。
单用户是否就完全没有同步问题
也不能绝对这么说。
即使只有一个用户,也可能存在:
- 同一账号多个 tab / 设备;
- 后台任务修改状态;
- 定时任务或 Agent 修改数据;
- Mutation 引发服务端计算,影响多个实体或聚合字段;
- 新增/删除导致列表 membership 变化;
- Mutation response 没有返回全部受影响数据。
因此单用户下,“当前客户端自己修改的单个实体字段”通常最容易直接同步;但涉及外部变化、派生状态、集合关系或后台处理时,仍可能需要 refetch 或实时事件。
为什么 Mutation 后需要额外思考
服务端写成功,不代表所有已缓存 Query 都自动得到正确的新结果。
如果返回了具有稳定 identity 的实体,Apollo 可能自动合并实体字段;但新增/删除列表元素、复杂派生查询等场景仍可能需要:
cache update
refetch
或更明确的 field policy
因此客户端一致性仍然是工程问题,不会因为使用 GraphQL 自动消失。
边界
Apollo Client 不应该成为业务 durable truth。
Database / Server state = authority
Apollo Cache = client projection
缓存可以丢弃、重建、重新拉取;真正业务状态仍由服务端拥有。
学习重点
先掌握五个问题:
- 一次 GraphQL Query 怎样进入 Apollo Client;
- Apollo 为什么需要 normalized cache;
- Mutation 响应怎样通过 entity identity 更新 cache;
- 什么时候应该直接 patch,什么时候应该 refetch;
- 当前客户端写入与外部实时变化为什么需要不同同步机制。