在 Relay 中,"获取数据"只是数据生命周期的一半。本指南基于 Relay 19 版本文档中 updating-data 章节入口文档 展开,系统讲解如何有意地修改 Relay 本地数据存储(Relay store)以及服务器端数据:包括使用useMutation/commitMutation执行 GraphQL Mutations、通过updater命令式改写 store、借助optimisticResponse实现乐观更新、用ConnectionHandler维护连接(Connections),以及通过commitLocalUpdate/commitPayload做纯本地更新。读完本文,你将掌握一套从"发出写请求"到"本地 store 与 UI 保持一致"的完整数据更新方案。
从"获取数据"到"更新数据":为什么需要专门讨论
在 guided-tour 的获取数据章节 中,我们讨论了如何用 GraphQL 查询获取数据。需要强调的是:获取数据这件事附带地也可能修改 Relay 本地 store 中的数据——如果查询返回的数据与 store 中已有缓存不一致,Relay 会合并并覆盖。但这不是"有意"的写入行为,因此我们还需要一套机制来显式地、受控地更新本地数据与服务端数据,这正是 updating-data 系列文档的主题。
在进入细节之前,先明确一个贯穿全文的核心概念:
:::noteRelay store是关联于某个 Relay environment 的 GraphQL 数据缓存,其中保存的是应用执行过程中遇到过的(即被查询过的)数据。所有本地写入最终都会落到这个 store 上,并驱动依赖这些数据的组件重渲染。 :::
Relay 更新数据的整体能力图谱(对应本仓库 versioned docs 中 updating-data 目录)如下:
- GraphQL Mutations:通过
useMutation/commitMutation执行服务端写操作,并将响应写回 store; - GraphQL Subscriptions:通过
useSubscription/requestSubscription订阅服务端事件流,随事件更新数据; - 命令式修改 store 数据:在
updater中使用readUpdatableFragment/readUpdatableQuery精确改写标量字段; - 命令式修改链接字段:通过
@assignable片段在 store 中重新指定记录间的链接关系; - 更新连接(Connections):向连接增删边,可用声明式指令或
ConnectionHandler手工操作; - 本地数据更新:
commitLocalUpdate与commitPayload两类纯本地 API; - 客户端专属数据:通过 client schema extensions 在客户端扩展 GraphQL schema;
- 类型安全 updaters FAQ:解释
@updatable/readUpdatableQuery等类型安全 API 的设计动机与约束。
下面按由简到繁的顺序逐一展开。
GraphQL Mutations:在 Relay 中声明并执行写操作
在 GraphQL 中,服务端数据通过mutation更新。Mutation 是一种"可读可写"的服务端操作:它既在后台修改数据,又允许在同一次请求中查询修改后的数据。
编写 Mutation
Mutation 的写法与 query 几乎一致,只是关键字换成mutation:
mutation FeedbackLikeMutation($input: FeedbackLikeData!) { feedback_like(data: $input) { feedback { id viewer_does_like like_count } } }- 上面的 mutation 修改服务端数据,把指定的
Feedback对象标记为"已点赞"; feedback_like是mutation 根字段(mutation root field),负责在后台更新数据;- Mutation 的处理分为两个独立步骤:先处理服务端更新,再执行查询。这保证了响应中只会出现"已经被本次 mutation 更新过"的数据。注:查询本身也是这种"外层先于内层"的计算顺序,只是约定上 mutation 顶层字段带有副作用;
- mutation 根字段返回一个具体的 GraphQL 类型,该类型暴露了可以在 mutation 响应中查询的数据。本例查询的是更新后的 feedback 对象,包括最新的
like_count和viewer_does_like(表示当前 viewer 是否已点赞)。
一次成功响应的示例:
{ "feedback_like": { "feedback": { "id": "feedback-id", "viewer_does_like": true, "like_count": 1, } } }在 Relay 中,同样用graphql标签来声明 mutation,并且 mutation 可以像 query/fragment 一样引用 GraphQL variables:
const {graphql} = require('react-relay'); const feedbackLikeMutation = graphql` mutation FeedbackLikeMutation($input: FeedbackLikeData!) { feedback_like(data: $input) { feedback { id viewer_does_like like_count } } } `;使用useMutation执行 mutation
Relay 提供了commitMutation和useMutation两个 API 来执行 mutation。先看 Hooks 风格的useMutation:
import type {FeedbackLikeData, LikeButtonMutation} from 'LikeButtonMutation.graphql'; const {useMutation, graphql} = require('react-relay'); function LikeButton({ feedbackId: string, }) { const [commitMutation, isMutationInFlight] = useMutation<LikeButtonMutation>( graphql` mutation LikeButtonMutation($input: FeedbackLikeData!) { feedback_like(data: $input) { feedback { viewer_does_like like_count } } } ` ); return <button onClick={() => commitMutation({ variables: { input: {id: feedbackId}, }, })} disabled={isMutationInFlight} > Like </button> }这段代码的要点:
useMutation的唯一参数是一个包含 mutation 的graphql字面量;- 它返回一个二元组:commit 回调(接受一个
UseMutationConfig配置对象)与布尔值isMutationInFlight(表示是否有 mutation 在途,可用于禁用按钮); useMutation还接受一个 Flow 类型参数(如LikeButtonMutation)。该类型由 Relay compiler 生成的graphql.js文件导出。最佳实践是始终提供这个类型参数——提供后UseMutationConfig也会被静态类型化;- 调用
commitMutation后,Relay 发起网络请求执行feedback_like字段:服务端记录用户已点赞,并回选更新后的viewer_does_like与like_count。由于Feedback类型包含id字段,Relay compiler 会自动为其补选id; - 响应到达后,Relay 在 store 中找到
id匹配的 feedback 记录并更新字段值;任何选择过这些字段的组件都会在值变化时自动重渲染。
类型命名规则:参数类型
FeedbackLikeData的名字派生自顶层 mutation 字段名(即feedback_like),同样由生成的graphql.js文件导出。
源码视角:useMutation的实现
在 React 侧,useMutation的实现位于 packages/react-relay/relay-hooks/useMutation.js。其中UseMutationConfig类型(L33-L51)完整定义了可以传给 commit 回调的配置项:
configs?: Array<DeclarativeMutationConfig>:声明式 mutation 配置(旧式);onError?: ?(error: Error) => void与onCompleted?: ?(response, errors) => void:成功/失败回调;optimisticResponse:乐观响应(依赖@raw_response_type指令获得类型);optimisticUpdater?: ?SelectorStoreUpdater与updater?: ?SelectorStoreUpdater:乐观/常规 updater 函数;uploadables?: UploadableMap:文件上传;variables:必填的 mutation 变量。
hook 签名(L66-L75)返回[(UseMutationConfigInternal) => Disposable, boolean]。其内部逻辑(L107-L120)是:调用 commit 时先setMutationInFlight(true),然后委托给commitMutationFn(默认是relay-runtime导出的commitMutation),并在onCompleted/onError时通过cleanup把该 disposable 从在途集合中移除、更新isMutationInFlight。这正是"按钮 disabled 状态随请求生命周期变化"的实现基础。
让组件响应 mutation:手动选字段 vs 展开 fragment
上例手动选择了viewer_does_like与like_count,选择这些字段的组件会在值变化时重渲染。但更推荐的做法是展开对应组件的 fragment——因为组件选择的数据集是会演进的,要求开发者记住"所有可能影响自己数据的 mutation"并持续维护,正是 Relay 想要避免的全局推理负担。
mutation FeedbackLikeMutation($input: FeedbackLikeData!) { feedback_like(data: $input) { feedback { ...FeedbackDisplay_feedback ...FeedbackDetail_feedback } } }这样,FeedbackDisplay与FeedbackDetail组件所选字段会在 mutation 执行后一并刷新,组件始终保持一致状态。
:::note 展开 fragment 通常优于 mutation 完成后再手动 refetch:更新后的数据可以在单次往返中取回。 :::
在 mutation 完成或失败时执行回调
UseMutationConfig支持以下回调:
onCompleted:mutation 成功完成时执行,参数是 mutation 响应(在 fragment 展开边界处截断)。传入的值是在 updaters 与声明式 mutation 指令应用之后、从 store 中读出的 mutation fragment——因此未展开 fragment 内的数据不会包含其中,被删除的记录(如@deleteRecord删除的)可能为null;onError:mutation 失败时执行,参数是发生的错误对象。
声明式 mutation 指令:删除记录与操作连接
除了手写 updater,Relay 提供了一组"声明式"指令,让常见更新几乎零代码。
删除记录:@deleteRecord
想在 mutation 响应中把某个对象从 store 删除,可在其id字段上加@deleteRecord:
mutation DeletePostMutation($input: DeletePostData!) { delete_post(data: $input) { deleted_post { id @deleteRecord } } }连接增删:@appendEdge/@prependEdge/@appendNode/@prependNode/@deleteEdge
这些指令(以及@deleteEdge)可以作用在 mutation、subscription 甚至 query 的字段上,具体细节见 Updating Connections 文档,此处先给出要点:
@appendEdge/@prependEdge:作用于返回单个 edge 或 edge 列表的字段,把选中的边追加到connections参数指定各连接的末尾/开头;@appendNode/@prependNode:作用于返回单个 node 或 node 列表的字段,把选中的节点包装成边后追加/前插;@deleteEdge:作用于返回ID或[ID]的字段,删除各连接中节点 id 匹配的边;- 它们都接受
connections参数——一个包含连接 ID 的 GraphQL 变量。连接 ID 可通过连接字段的__id或ConnectionHandler.getConnectionIDAPI 获取。
命令式修改 store:updater函数
当更新逻辑比"写入响应字段"更复杂,且声明式指令无法覆盖时,UseMutationConfig接受updater函数,把 store 的写入完全交给你控制。详见 Imperatively modifying store data。
什么时候用 updater
- 复杂的客户端更新:比简单地把网络响应写进 store 更复杂、且声明式指令搞不定的场景;
- 客户端 schema 扩展字段的初始化:网络响应必然不包含 client schema extensions 中定义的字段,可以用 updater 为其初始化数据;
- 其他 API 无法完成的操作:如失效节点、删除节点、按字段查找所有连接等;
- 多个乐观响应修改同一个 store 值时:若两个乐观响应各把点赞数 +1,第一个被回滚后,第二个仍生效且不会被重算,点赞数会保持 +2——这种叠加场景下应使用乐观 updater(详见下文"乐观更新")。
什么时候不要用 updater
- 触发其他副作用时:应该用
onCompleted回调。onCompleted保证只被调用一次,而 updater/optimistic updater 可能被多次调用。
类型安全的 updater:readUpdatableFragment与@updatable
现代(推荐)写法是"可更新的 fragment/query + 类型安全的代理对象"。以下例演示:通过 schema extension 给Feedback增加is_new_comment字段,并在 mutation updater 中把它置为true。
首先定义 schema 扩展:
# Feedback.graphql extend type Feedback { is_new_comment: Boolean }然后在 mutation 的响应中展开一个@updatable片段,并在updater中读出并改写它:
// CreateFeedback.js import type {Environment} from 'react-relay'; import type { FeedbackCreateData, CreateFeedbackMutation, } from 'CreateFeedbackMutation.graphql'; const {commitMutation, graphql} = require('react-relay'); function commitCreateFeedbackMutation( environment: Environment, input: FeedbackCreateData, ) { return commitMutation<FeedbackCreateData>(environment, { mutation: graphql` mutation CreateFeedbackMutation($input: FeedbackCreateData!) { feedback_create(input: $input) { feedback { id # Step 1: 在 mutation 响应中展开 updatable 片段 ...CreateFeedback_updatable_feedback } } } `, variables: {input}, // Step 2: 定义 updater updater: (store, response) => { // Step 3: 访问并判空 feedback 对象 const feedbackRef = response?.feedback_create?.feedback; if (feedbackRef == null) { return; } // Step 4: 用 store.readUpdatableFragment 读出可更新的代理对象 const {updatableData} = store.readUpdatableFragment( graphql` fragment CreateFeedback_updatable_feedback on Feedback @updatable { is_new_comment } `, feedbackRef ); // Step 5: 直接给代理对象的字段赋值! updatableData.is_new_comment = true; }, }); }这段代码的关键事实:
updater接收两个参数:RecordSourceSelectorProxy(store 代理)与一个可选对象——即 mutation 响应的读取结果。第二个参数的类型是生成文件导出的$data类型的可空版本,且只包含 mutation 直接选中的字段(不含 fragment 展开出来的字段);- 该 updater 在 mutation 响应写入 store之后执行;
readUpdatableFragment返回带updatableData字段的代理对象,对其字段赋值即记录到 store;updater 完成后,所有受影响组件自动重渲染。
替代 API:readUpdatableQuery
如果知道从根(Query类型)到目标记录之间的路径,可以用readUpdatableQuery以 query 形式读写,例如在表单提交时更新 viewer 的name:
commitLocalUpdate(environment, store => { const {updatableData} = store.readUpdatableQuery( graphql` query NameUpdaterUpdateQuery @updatable { viewer { name } } `, {} ); const viewer = updatableData.viewer; if (viewer != null) { viewer.name = newName; } });优先选择readUpdatableQuery的理由:
- 手头没有现成的 fragment reference(例如
commitLocalUpdate调用与某个组件没有直接关联); - 手头没有能选中"目标记录的父记录"的 fragment(由于 Relay 中一个已知的类型空洞,updatable fragment 不能在顶层展开);
- 想在 updatable fragment 中使用变量。目前 updatable fragment 复用 query 传入的变量,无法用 fragment 局部变量多次调用
readUpdatableFragment。
修改链接字段:@assignable片段
readUpdatableQuery不仅能改标量字段,还能重新指定记录间的链接(如把某个用户设为 viewer 的best_friend)。要点是引入@assignable片段:
extend type Viewer { best_friend: User, }// 可赋值的片段只能包含单个字段 __typename graphql` fragment AssignBestFriendButton_assignable_user on User @assignable { __typename } `;该片段必须在赋值来源(新 best friend 上)和赋值目标(updatable query 中 viewer 的best_friend字段下)同时展开,然后即可安全赋值:
const environment = useRelayEnvironment(); const onClick = () => { commitLocalUpdate( environment, (store) => { const {updatableData} = store.readUpdatableQuery( graphql` query AssignBestFriendButtonUpdatableQuery @updatable { viewer { best_friend { ...AssignableBestFriendButton_assignable_user } } } `, {} ); if (data.user != null && updatableData.viewer != null) { updatableData.viewer.best_friend = data.user; } } ); };关于链接字段赋值有一条硬性约束:被赋值的链接字段(赋值表达式的右侧)必须源自只读的 fragment/query/mutation/subscription。也就是说updatableData.foo = updatableData.foo是非法的;若要把列表追加元素,也必须先从只读 fragment 中取出原列表,再执行viewer.best_friends = existing_list.concat([newBestFriend])。从抽象类型(如Node)向具体/接口类型字段赋值时,还需要用内联 fragment 做类型收窄(必要时配合编译器生成的validator导出做运行时校验)。这些进阶细节都在 Imperatively modifying linked fields 中。
为什么需要"类型安全 updaters"这套 API
Typesafe updaters FAQ 给出了设计动机:旧式 updater API 冗长、非类型安全、容易出错,还要求开发者只为写 updater 学一套新方法。类型安全 updaters 复用 Relay 已有的 query/fragment/类型收窄习惯,用 getter/setter(updatableData.name = "Godzilla")代替记忆各种方法,赋值操作底层仍会转成旧 API 调用,但获得了静态类型保障。
同时需要明确@updatable查询/片段的特性:
- 它们不会被真正从服务器获取。服务端不认识 updatable query/fragment,即使把 updatable fragment 展开在普通 query 里,其字段也不会随请求下发;
- 想"既获取又修改"某个字段,需要在普通 query/fragment和updatable query/fragment 中同时选中它;
- 由此带来几个后果:读取 updatable 数据时可能因 store 中缺失而为空;updatable query/fragment 中不能展开普通 fragment;其生成产物不含 query ID 与 normalization AST(normalization AST 用于把网络数据写入 store);
@defer等指令在 updatable 上下文中没有意义、被禁止。
乐观更新:optimisticResponse与optimisticUpdater
很多时候我们不想等服务器响应才反馈用户操作。比如点击"Like"后希望立即展示点赞效果——在假设 mutation 必然成功的前提下先把数据写入 store,这就是乐观更新;若最终失败,则回滚该乐观更新。
乐观响应(optimisticResponse)
在UseMutationConfig中加入optimisticResponse字段即可。为了让该字段获得 Flow 类型,useMutation必须传入类型参数并且mutation 需带@raw_response_type指令:
mutation LikeButtonMutation($input: FeedbackLikeData!) @raw_response_type { feedback_like(data: $input) { feedback { viewer_does_like like_count } } }commitMutation({ variables: {input: {id: data.__id}}, optimisticResponse: { feedback_like: { feedback: { // 虽然 id 未显式选中,编译器已为我们补选 id: data.__id, viewer_does_like: !data.viewer_does_like, like_count: data.like_count + changeToLikeCount, }, }, }, })调用commitMutation后,这份数据会被立即写入 store:匹配 id 的记录获得新的viewer_does_like值,订阅该字段的组件立刻重渲染。当 mutation 成功或失败时,乐观响应会被回滚。
注意:上面的
like_count计算依赖组件中读取到的当前点赞数——为此需要在 fragment 中用@required(action: THROW)强制读取viewer_does_like与like_count(详见 graphql-mutations 文档 的完整示例)。
乐观响应存在不少陷阱,需谨慎使用:
- 乐观响应可以包含整个 query 响应的数据(包括 fragment 展开的内容)。如果开发者后续在组件 fragment 中新增了字段,乐观更新期间这些组件可能拿到不一致或部分的数据;
- 由于乐观响应的类型包含所有递归嵌套 fragment 的内容,类型可能非常庞大;给 mutation 加
@raw_response_type会拖慢 Relay compiler。
另外,若乐观响应的新值依赖 store 当前值、且可能有多个乐观响应同时影响该值,应改用乐观 updater:例如两个乐观响应各把点赞数 +1,当第一个被回滚时,第二个仍然生效,store 中的点赞数会保持 +2。
乐观 updater(optimisticUpdater)
当乐观数据不在 mutation 选中的字段里、或需要向连接增删边(而声明式指令不够用)时,UseMutationConfig还支持optimisticUpdater,允许在乐观阶段命令式改写 store——与普通updater的写法一致,详见 Imperatively updating store data。
updater 系列的执行顺序
无论成功失败,mutation 涉及的更新按以下顺序执行:
- 若提供了
optimisticResponse,其数据先写入 store; - 若提供了
optimisticUpdater,Relay 执行它并按结果更新 store; - 若提供了
optimisticResponse,对乐观响应处理 mutation 中的声明式指令; - 若 mutation 请求成功:
- 回滚此前应用的乐观更新;
- 把服务器响应写入 store;
- 若提供
updater,执行之(服务器 payload 作为 store 中的一个根字段可供其读取); - 用服务器响应处理声明式指令;
- 调用
onCompleted。
- 若 mutation 请求失败:
- 回滚此前应用的乐观更新;
- 调用
onError。
更新连接(Connections):增删边与连接标识
渲染连接(如评论列表)时,通常还需要在用户操作后向连接增加或移除条目。由于 store 中带@connection指令的连接字段以特殊记录保存,并累积了该连接所有已获取条目,操作它们需要先取得连接记录。完整指南见 Updating Connections,实现位于 packages/relay-runtime/handlers/connection/。
三种方式获取连接记录
// 方式一:查询连接的 __id 字段(该字段由 Relay 自动附加,服务端无需暴露) const connectionID = fragmentData?.comments?.__id; // 方式二:由父记录 ID + 连接 key 计算连接 ID const connectionID = ConnectionHandler.getConnectionID( storyID, 'StoryComponent_story_comments_connection', ); // 方式三:由父记录对象直接取连接记录 const storyRecord = store.get(storyID); const connectionRecord = ConnectionHandler.getConnection( storyRecord, 'StoryComponent_story_comments_connection', );添加边
声明式方式:在 mutation/subscription 返回 edge 或 node 的字段上使用@appendEdge/@prependEdge/@appendNode/@prependNode(也适用于 query),并通过connections变量传入连接 ID 数组:
commitMutation<AppendCommentMutation>(environment, { mutation: graphql` mutation AppendCommentMutation( $connections: [ID!]! $input: CommentCreateInput ) { commentCreate(input: $input) { feedbackCommentEdge @appendEdge(connections: $connections) { cursor node { id } } } } `, variables: { input, connections: [connectionID], }, });手工方式:在 updater 中先用store.getRootField('comment_create')读取 mutation payload 根字段,再用ConnectionHandler.buildConnectionEdge(store, connectionRecord, serverEdge)构造边,最后用ConnectionHandler.insertEdgeAfter/insertEdgeBefore插入(这两个 API 会原地修改连接)。若要从零创建边,则用store.create创建本地记录 +ConnectionHandler.createEdge(store, connectionRecord, newCommentRecord, 'CommentEdge')。
删除边
- 声明式:在返回被删节点 ID(
ID或[ID])的字段上加@deleteEdge(connections: $connections); - 手工:
ConnectionHandler.deleteNode(connectionRecord, commentIDToDelete),它会根据节点 ID 找到对应边并移除。
无论用哪种方式修改连接,渲染该连接的所有 fragment/query 组件都会收到通知并以最新连接状态重渲染。
带过滤条件的连接标识
如果连接字段带过滤参数(如order_by、filter_mode、language),这些过滤值会参与连接标识——分页参数(first/last/before/after)除外。也就是说,过滤值的每种组合都会生成不同的连接记录。此时ConnectionHandler.getConnection需要传入第三个参数:
ConnectionHandler.getConnection( storyRecord, 'StoryComponent_story_comments_connection', {order_by: '*DATE_ADDED*', filter_mode: null, language: null} );当过滤组合多到难以管理时,可以在@connection中显式指定参与标识的filters数组:
comments( order_by: $orderBy filter_mode: $filterMode language: $language ) @connection( key: "StoryComponent_story_comments_connection" filters: ["order_by", "filter_mode"] ) { edges { nodes { body { text } } } }这样只有order_by与filter_mode影响连接身份,language变化不会产生新连接记录——前提是它确实不改变服务端返回的条目集合或排序(或该参数在应用中永不变化)。
纯本地数据更新:commitLocalUpdate与commitPayload
有些更新完全不涉及服务端操作。Relay 为此提供了两个 API(详见 Local Data Updates),它们既可以作用于客户端专属数据,也可以作用于从服务端取回的普通数据。
commitLocalUpdate
接受一个 environment 与一个 updater 函数:
const {commitLocalUpdate, graphql} = require('react-relay'); function commitCommentCreateLocally( environment: Environment, feedbackID: string, ) { return commitLocalUpdate(environment, store => { // 在这里命令式地修改 store }); }- updater 的
store参数是RecordSourceSelectorProxy实例(完整 API 见 api-reference/store),可以命令式读写 store:创建全新记录、更新或删除既有记录; - 与 mutation/subscription 的 updater 不同,
commitLocalUpdate的 updater不接受第二个参数——因为它没有关联的网络响应; - 本地更新会自动通知订阅了相关数据的组件并触发重渲染。
commitPayload
接受一个OperationDescriptor与 query 的 payload,将其写入 store。payload 会像普通 query 的网络响应一样被归一化处理,也会解析以JSResource、requireDefer等传入的 Data Driven Dependencies:
import type {FooQueryRawResponse} from 'FooQuery.graphql' const {createOperationDescriptor} = require('relay-runtime'); const operationDescriptor = createOperationDescriptor(FooQuery, { id: 'an-id', otherVariable: 'value', }); const payload: FooQueryRawResponse = {...}; environment.commitPayload(operationDescriptor, payload);OperationDescriptor由createOperationDescriptor(query, variables)创建;- payload 的类型来自给 query 加
@raw_response_type指令后生成的 Flow 类型; - 同样地,任何本地更新都会自动通知订阅组件并触发重渲染。
客户端专属数据(Client Schema Extensions)
Relay 允许在浏览器端扩展 GraphQL schema,用来建模只在客户端创建/读取/更新的数据——例如给服务端取回的数据附加小块本地信息,或完整建模一段客户端专属状态。详见 Client-only data。
扩展既有类型:
extend type Comment { is_new_comment: Boolean }新增客户端专属类型(同一文件可定义多个类型,且可引用服务端类型):
enum FetchStatus { FETCHED PENDING ERRORED } type FetchState { status: FetchStatus started_by: User! } extend type Item { fetch_state: FetchState }读取客户端专属数据与读取普通字段完全一致(在 fragment/query 中直接选中);更新它们则可以在 mutation/subscription 的 updater 中完成,或使用上文介绍的本地更新原语(commitLocalUpdate/commitPayload)。
GraphQL Subscriptions:订阅服务端事件流
除了 mutation 这种"客户端主动发起"的写入,Relay 还支持通过GraphQL subscriptions响应服务端事件流。完整指南见 GraphQL subscriptions。
声明订阅
订阅与 query 类似,只是使用subscription关键字:
subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { feedback_like_subscribe(data: $input) { feedback { like_count } } }feedback_like_subscribe是subscription 根字段,负责在后台建立订阅;- 与 mutation 类似,订阅分两步处理:先发生服务端事件,再执行查询;
- 注意事件流可以与所选字段完全无关——不保证每次通知所选值都发生了变化。
使用useSubscription
const {graphql, useSubscription} = require('react-relay'); const {useMemo} = require('React'); function useFeedbackSubscription(input: FeedbackLikeSubscribeData) { const config = useMemo(() => ({ subscription: graphql` subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { feedback_like_subscribe(data: $input) { feedback { like_count } } } `, variables: {input}, }), [input]); return useSubscription(config); }要点:
useSubscription接受一个GraphQLSubscriptionConfig对象,包含subscription(订阅字面量)与variables(建立订阅所用的变量);- 同样建议传入 Flow 类型参数以获得静态类型;
- 与
useLazyLoadQuery不同,Relay 不会在渲染阶段建立订阅;订阅在 hook 提交后建立; - 每次事件到达,Relay 按
id匹配 store 中的记录并更新,相关组件自动重渲染。
⚠️ 传给
useSubscription的GraphQLSubscriptionConfig必须被 memoized(如上例用useMemo),否则useSubscription会在每次渲染时销毁并重建订阅!
GraphQLSubscriptionConfig还支持回调字段:onNext(收到 payload 时)、onError(出错时)、onCompleted(服务端结束订阅时)。声明式指令(@deleteRecord、连接增删指令)同样适用于订阅。订阅需要网络层支持,通常在 network-layer 指南 中配置 WebSocket 传输(如graphql-ws),并把subscribe函数通过Network.create(fetchQuery, subscribe)注入。
在 mutation 期间失效数据(Staleness)
执行 mutation 的推荐做法是:在 mutation 体内把受影响的数据全部请求回来,让本地 store 与服务端保持一致。但某些"涟漪效应"巨大的 mutation(如"屏蔽用户""退出群组")很难枚举全部受影响数据。此时更直接的做法是显式标记部分数据(或整个 store)为过期,让 Relay 在下次渲染时自动 refetch。相关 API 见 Staleness of Data 章节。
小结
Relay 的数据更新体系围绕"本地 store 与服务端保持一致"这一核心目标设计,形成了一条清晰的能力阶梯:
- 简单场景优先用声明式手段——mutation 响应自动归一化写入 store、
@deleteRecord删除记录、@appendEdge等指令操作连接; - 中等场景用
updater/optimisticResponse在响应写入前后微调 store,控制执行时机与回滚语义; - 复杂场景用类型安全的
readUpdatableQuery/readUpdatableFragment(@updatable)与@assignable片段精确改写标量字段与链接关系; - 纯本地状态用
commitLocalUpdate/commitPayload与 client schema extensions 建模客户端专属数据; - 服务端推送用
useSubscription订阅事件流,配合 WebSocket 网络层。
无论走哪条路径,最终效果都是一致的:store 中的记录被更新,订阅了相关字段的组件自动重渲染——这正是 Relay"局部写、全局一致"的数据驱动哲学在写入侧的体现。若要继续深入,推荐按顺序阅读 GraphQL mutations、命令式修改 store 数据 与 更新连接 三篇文档,并结合 useMutation 源码 与 ConnectionHandler 实现 理解底层行为。
- 前端
- 开发工具
【免费下载链接】relay
Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay
相关推荐
Relay GraphQL Mutations 完全指南:从 useMutation 到乐观更新与 Store 数据同步
Relay GraphQL Mutations 完全指南:从 useMutation 到乐观更新与 Store 数据同步 本指南基于仓库中的官方文档 graph
前端开发工具Relay 数据更新指南:Mutation、Subscription 与本地 Store 命令式修改
Relay 数据更新指南:Mutation、Subscription 与本地 Store 命令式修改 本篇技术指南以 Relay(Relay is a Java
前端开发工具Relay 18 类型安全 Store 更新指南:使用 readUpdatableQuery / readUpdatableFragment 命令式修改 Store 数据
Relay 18 类型安全 Store 更新指南:使用 readUpdatableQuery / readUpdatableFragment 命令式修改 Sto
前端开发工具