ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Relay 命令式修改 Store 数据:updater 函数完整实战指南(legacy 版)

Relay 命令式修改 Store 数据:updater 函数完整实战指南(legacy 版) 前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载本篇指南聚焦 Relay 数据更新机制中一个关键能力在 updater 函数内以命令式imperative方式直接读写 Relay Store从而完成声明式指令和普通网络响应无法覆盖的复杂本地数据更新。你将掌握 updater 的适用场景与禁用场景、optimisticUpdater与updater的执行时机差异、基于RecordSourceSelectorProxy/ConnectionHandler的完整可运行示例以及 v19 中与之对应的 typesafe 替代方案能够直接在你的 Relay 应用中落地。updater 是什么在 Relay Store 上获得“完全控制权”Relay 在内存中维护一份归一化normalized的 GraphQL 数据 Store所有记录record按dataID存储。通常情况下数据流向是网络响应 → 归一化 → 写入 Store → 订阅了对应数据的组件收到通知并重渲染这一过程由 Relay 自动完成。但 Relay 也开放了一条命令式通道在 updater 函数中你可以直接读取和写入 Store 数据。updater 接收的第一个参数store是RecordSourceSelectorProxy实例该接口允许你对 Store 进行完全的命令式操作——你可以创建全新的记录、更新已有记录、删除记录从而以编程方式精确控制“收到变更响应后 Store 如何变化”。这一点在 Store API 参考中有明确描述Data in Relay stores can be imperatively modified within updater functions.什么时候应该使用 updater复杂客户端更新当本地数据的变更比“直接把网络响应写入 Store”更复杂且无法由声明式 mutation 指令如appendEdge、deleteEdge、deleteRecord处理时就应该提供 updater 函数。例如一次 mutation 返回后需要同时更新多个互不关联的记录、需要依据 Store 中已有值做条件判断、或者需要把响应数据重新组织后再写入。客户端 Schema 扩展Client Schema Extensions字段的初始化网络响应必然不会包含在客户端 schema 扩展例如extend type Feedback { is_new_comment: Boolean }中定义的字段数据。因此当这类字段需要随 mutation 一起初始化时updater 是最直接的选择——你可以在响应写入 Store 后立即为扩展字段写入合适的初始值。这也是 新版 typesafe updater 指南中第一个示例的动机。其他 API 的不可替代性还有一部分操作只能通过 updater 完成例如失效invalidate节点调用record.invalidateRecord()或store.invalidateStore()删除节点调用store.delete(dataID)查找某个字段下的所有连接connection借助ConnectionHandler在给定字段上定位连接记录。这些能力都没有对应的声明式替代品属于 updater 的“独占领地”。多个乐观响应修改同一个 Store 值时的行为这是一个重要的边界情况如果两个乐观响应optimistic responses影响了同一个值而第一个乐观响应被回滚第二个依然保持生效。举例两个乐观响应各自把某条 story 的点赞数likeCount增加 1。当第一个乐观响应被回滚时第二个乐观响应仍然生效由于第二个乐观响应不会被重新计算点赞数最终会停留在“增加了 2”的状态而不是恢复为“增加了 1”。与之相对乐观 updateroptimistic updater在这种情况下会被重新执行。因此如果乐观更新的值依赖于 Store 中已有的其他值、且存在多个乐观响应可能同时影响该值官方建议使用乐观 updater 而非纯optimisticResponse参见 graphql-mutations.md 中关于乐观更新的讨论。什么时候不应该使用 updater触发其他副作用updater 是纯 Store 变更逻辑不应用于触发副作用side effects如弹出提示、跳转路由、埋点上报。这类逻辑应放在onCompleted回调中——该回调保证在 mutation 完成后恰好调用一次而 updater / optimistic updater 可能被重复调用例如乐观 updater 在 mutation 失败回滚后不再执行、但在重试等场景下可能再次进入相关流程不具备“恰好一次”的语义保证。updater 函数的各种类型与入口 API不同的入口 API 支持不同的 updater 字段需要区分清楚API支持的 updater 字段说明useMutation/commitMutationoptimisticUpdater、updatermutation 场景两类都支持requestSubscription/useSubscriptionupdater订阅场景仅支持常规 updatercommitLocalUpdateupdater 函数参数只有一个store本地数据更新无网络 payload详见 local-data-updates.md从类型定义看useMutation和commitMutation接受的配置对象即MutationConfig其 MutationConfig.md 中明确了两个字段optimisticUpdater类型为SelectorStoreUpdater在commitMutation被调用、且optimisticResponse已归一化写入 Store 之后执行updater类型为SelectorStoreUpdater在收到服务端 payload、且该 payload 已写入 Store 之后执行。而 SelectorStoreUpdater.md 给出了函数签名(store: RecordSourceSelectorProxy, data) void并明确指出该接口允许“命令式地直接读写 Relay Store”可以创建全新记录也可以更新或删除已有记录。乐观 updater vs 常规 updater执行时机与完整顺序Mutation 可以同时携带乐观 updater 和常规 updater二者的语义截然不同乐观 updateroptimistic updater在 mutation 被触发时立即执行。当 mutation完成或失败时乐观更新被回滚随后服务端响应被写入 Store常规 updater 执行。常规 updaterupdater在 mutation成功完成后执行。graphql-mutations.md 给出了完整的执行顺序这里完整列出如果提供了optimisticResponse先将该数据写入 Store如果提供了optimisticUpdaterRelay 执行它并更新 Store如果提供了optimisticResponse处理 mutation 中的声明式 mutation 指令作用于乐观响应mutation 请求成功时回滚已应用的乐观更新将服务端响应写入 Store如果提供了updaterRelay 执行它并更新 Store——此时服务端 payload 在 Store 中以**根字段root field**形式可供 updater 读取使用服务端响应处理声明式 mutation 指令调用onCompleted回调。mutation 请求失败时回滚已应用的乐观更新调用onError回调。一个值得注意的细节来自 MutationConfig.mdonCompleted收到的是“在 updater 和声明式指令应用之后从 Store 读出的 mutation 片段”因此被deleteRecord删除的记录在其中可能为 null。实战示例为commitMutation提供 updater 添加评论下面是一个完整的可运行示例评论创建成功后将服务端返回的新 edge 插入到本地已有的评论连接中。它完整保留了原文档的代码骨架并补充了类型与注释import type {Environment} from react-relay; import type {CommentCreateData, CreateCommentMutation} from CreateCommentMutation.graphql; const {commitMutation, graphql} require(react-relay); const {ConnectionHandler} require(relay-runtime); function commitCommentCreateMutation( environment: Environment, feedbackID: string, input: CommentCreateData, ) { return commitMutationCreateCommentMutation(environment, { mutation: graphql mutation CreateCommentMutation($input: CommentCreateData!) { comment_create(input: $input) { comment_edge { cursor node { body { text } } } } } , variables: {input}, updater: (store: RecordSourceSelectorProxy, _response: ?CreateCommentMutation$data) { // 本例未使用 _response但它被提供且是静态类型化的 // 1. 获取父记录Feedback const feedbackRecord store.get(feedbackID); // 2. 通过连接 key 获取连接记录 const connectionRecord ConnectionHandler.getConnection( feedbackRecord, CommentsComponent_comments_connection, ); // 3. 获取服务端返回的 payloadmutation 根字段 const payload store.getRootField(comment_create); // 4. 获取 payload 内的 edge const serverEdge payload.getLinkedRecord(comment_edge); // 5. 基于服务端 edge 构建可插入连接的 edge const newEdge ConnectionHandler.buildConnectionEdge( store, connectionRecord, serverEdge, ); // 6. 将 edge 追加到连接末尾 ConnectionHandler.insertEdgeAfter( connectionRecord, newEdge, ); }, }); } module.exports {commit: commitCommentCreateMutation};逐点剖析这个示例究竟做了什么store参数是RecordSourceSelectorProxy实例。这是 updater 的命令式读写入口让你在响应 mutation 时对 Store 拥有完全控制可以创建全新记录、更新已有记录、删除记录。第二个data参数包含 mutation 片段直接选中的数据可用于不经过store直接取 payload。其类型从自动生成的Mutation.graphql.js文件中导入命名为MutationName$data。需要注意两点该data参数的类型是$data类型的可空版本data只包含 mutation直接选中的字段——如果 mutation 中展开了其他 fragment那些 fragment 的数据默认不会出现在data里这正是 legacy updater 容易踩的坑之一也是 typesafe updater 通过updatablefragment 解决的痛点。本例的实质是“向连接添加新项”在服务端成功添加评论后把新评论插入本地连接。连接的增删改细节可参考 updating-connections.md。特别地本例其实不需要手写 updater——这是使用appendEdge指令的理想场景mutation 响应是 Store 中的一个根字段记录可通过store.getRootField读取。此处读取的是comment_create即 mutation 响应中的根字段。mutation 的 root 与 query 的 root 不同在 mutation updater 中store.getRootField只能取得 mutation 响应中的记录若要访问不在 mutation 响应中的根级数据应改用store.getRoot().getLinkedRecord。updater 完成后自动触发重渲染updater 产生的本地数据变更会自动通知订阅了这些数据的组件并触发重渲染无需手动触发。ConnectionHandler 的源码级原理示例中使用的ConnectionHandler位于 packages/relay-runtime/handlers/connection/ConnectionHandler.js理解其内部实现有助于把握连接更新的本质getConnection(record, key, filters?)内部通过getRelayHandleKey(connection, key, null)计算出 handleKey然后调用record.getLinkedRecord(handleKey, filters)返回连接记录。也就是说connection指令声明的连接在 Store 中实际上是以父记录上的一个“handle 字段”存储的参见 ConnectionHandler.js#L283-L290。getConnectionID(recordID, key, filters?)用getStableStorageKey(handleKey, filters)生成稳定的存储 key再通过generateClientID(recordID, storageKey)计算连接记录的 ID——这就是为什么带不同过滤参数的同名连接会得到不同的连接 ID详见 ConnectionHandler.js#L323-L331。buildConnectionEdge(store, connection, edge)为服务端返回的 edge 创建一份带唯一 ID的副本。它利用连接记录上的__connection_next_edge_index计数器生成基于索引的客户端 ID避免同一连接重复拉取时 edge ID 冲突详见 ConnectionHandler.js#L549-L576。insertEdgeAfter(record, newEdge, cursor?)无cursor时直接把新 edge 追加到edges数组末尾有cursor时遍历现有 edges找到 cursor 匹配的 edge 后插入其后找不到则仍追加到末尾详见 ConnectionHandler.js#L367-L402。insertEdgeBefore是对称的前插实现。store参数的核心 API 速查RecordSourceSelectorProxy的完整接口定义见 store.md核心方法如下interface RecordSourceSelectorProxy { create(dataID: string, typeName: string): RecordProxy; // 创建新记录 delete(dataID: string): void; // 删除记录 get(dataID: string): ?RecordProxy; // 按 ID 取记录 getRoot(): RecordProxy; // 取文档根query 根 getRootField(fieldName: string): ?RecordProxy; // 取 mutation 响应根字段 getPluralRootField(fieldName: string): ?Array?RecordProxy; // 取复数根字段 invalidateStore(): void; // 全局失效 Store }返回的RecordProxy则提供了读写记录字段的能力getValue/setValue标量字段、getLinkedRecord/setLinkedRecord单值关联字段、getLinkedRecords/setLinkedRecords列表关联字段、getOrCreateLinkedRecord不存在则创建、copyFieldsFrom字段拷贝、getDataID、getType、invalidateRecord等。完整的逐方法说明与示例代码都在 store.md 中。声明式指令多数场景下的现代替代方案在投入手写 updater 之前值得先确认声明式 mutation 指令能否覆盖需求。Relay 提供了appendEdge、prependEdge、appendNode、prependNode和deleteEdge等指令它们可以直接作用于 mutation / subscription / query 响应中的字段把新 edge 追加/前插到指定连接、或按节点 ID 删除 edge从而免去手写ConnectionHandler样板代码详见 updating-connections.md。但这些指令提供的控制力有限无法覆盖所有用例例如需要按条件决定是否插入某条连接如“仅当评论来自好友时才插入好友专属连接”、需要同时维护多个不同过滤参数下的连接记录、或需要对 Store 做与连接无关的更复杂变更。此时手写 updater 仍是正确选择。关于“连接身份connection identity”与过滤参数的关系——每个非分页过滤参数的取值组合都会产生独立的连接记录ConnectionHandler.getConnection需传入第三个参数filters来定位——详见 updating-connections.md。从 legacy 到 typesafev19 中的升级路径需要说明的是本指南对应的 legacy 文档标题即带有(unsafe)后缀slug 为imperatively-modifying-store-data-unsafe。v19 同时提供了typesafe updater方案见 imperatively-modifying-store-data.md核心差异在于在 mutation 响应中展开带updatable指令的 fragment然后调用store.readUpdatableFragment(fragment, fragmentReference)拿到类型安全的updatableData代理对象直接对字段赋值如updatableData.is_new_comment true即可或者已知从根到目标记录的路径时用store.readUpdatableQuery(query, variables)完成同样操作如修改viewer.name这些 API 也出现在RecordSourceProxy的接口中见 store.md且比手写setValue更具类型安全性。readUpdatableQuery还有两个 legacy 方案没有的优势不依赖手头有 fragment reference例如commitLocalUpdate与组件无明确关联时且可以规避 Relay 的一个已知类型漏洞——updatablefragment 无法在顶层展开。需要给 updatable fragment 传 fragment 局部变量时readUpdatableFragment目前也受限updatable fragment 会复用 query 的变量这些取舍细节都记录在 imperatively-modifying-store-data.md。总结updater 函数是 Relay 数据更新体系中最底层、最灵活的一环它把 Store 的命令式读写权交给你用来完成复杂客户端更新、初始化客户端 schema 扩展字段、以及失效/删除/连接查找等独占操作。使用时要牢记三条边界副作用交给onCompleted、乐观更新优先考虑可重算的 optimistic updater、能被appendEdge等声明式指令覆盖的场景不必手写 updater。理解RecordSourceSelectorProxy与ConnectionHandler的实现细节连接记录实为父记录上的 handle 字段、连接 ID 由 key filters 稳定生成、edge 副本依赖自增索引避免 ID 冲突能帮你写出正确、高效且可维护的 Store 更新逻辑。完整 API 请参阅 Store API 参考升级迁移可对照 typesafe updater 指南。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay 命令式修改 Store 数据updater 函数完整指南基于 Relay v15 的 legacy 用法Relay 命令式修改 Store 数据updater 函数完整指南基于 Relay v15 的 legacy 用法 Relay 将本地数据写入与网络响应前端开发工具Relay 指南在 updater 函数中命令式修改 Store 数据Relay 指南在 updater 函数中命令式修改 Store 数据 Relay 采用归一化normalized的本地内存 Store 来缓存 Grap前端开发工具Relay 16 数据更新指南在 updater 中命令式修改 Store 数据legacy 方式Relay 16 数据更新指南在 updater 中命令式修改 Store 数据legacy 方式 Relay 的 store 是应用本地数据的中枢。除了前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表