Relay Typesafe Updaters 全面指南:用 `readUpdatableQuery` 与 `readUpdatableFragment` 安全地命令式修改 Store 数据
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载Relay 的 Typesafe Updaters类型安全更新器是一套在 store 上命令式imperatively修改本地数据的类型安全且更符合人体工程学的 API 体系核心由readUpdatableQuery与readUpdatableFragment两个入口构成。本文以官方 FAQ 为骨架结合仓库源码与实战示例讲清它解决了什么问题、底层如何工作、有哪些使用约束以及在哪里拿到store来调用这两类 API帮助你安全地管理客户端本地状态例如 client schema extension 中的字段。Typesafe Updaters 是什么项目背景与命名由来Typesafe updaters类型安全更新器是一个项目的名字目标是提供一套类型安全typesafe且符合人体工程学ergonomic的替代 API用于在 Relay store 上命令式地更新数据。readUpdatableFragment和readUpdatableQuery就是 store 暴露出的两个核心 typesafe updater 入口它们的完整签名定义在 store API 参考文档 中readUpdatableFragmentTFragmentType: FragmentType, TData( fragment: UpdatableFragmentTFragmentType, TData, fragmentReference: HasUpdatableSpreadTFragmentType, ): UpdatableDataTData; readUpdatableQueryTVariables: Variables, TData( query: UpdatableQueryTVariables, TData, variables: TVariables, ): UpdatableDataTData;为什么需要它WhyRelay 在“获取和管理来自服务端的数据”这一侧提供了类型安全且易用的 API同时Relay 也支持在client schema extensions中定义仅存在于客户端的字段。然而过去用于修改这些字段数据的 API 冗长且不友好以至于官方无法将 Relay 推荐为管理本地状态的方案。Typesafe updaters 正是为了补上这块短板。旧 API 的问题在哪里旧有的命令式更新 API 存在两个明显的缺陷冗长verbose需要开发者写出大量样板代码。非类型安全not typesafe极易犯下各类低级错误例如字段名拼错、类型不匹配等。更关键的是旧 API 要求开发者只有在编写 updater 时才需要去学习一套全新的 API 集合例如setValue、setLinkedRecord、getLinkedRecord等RecordProxy方法学习成本高且与日常开发模式割裂。Typesafe updaters 的优势在于复用 Relay 早已为人熟知的习惯用法query、fragment、类型收窄type refinement用getter 与 setter属性读写取代需要单独记忆的方法集——updatableData.name Godzilla这种写法对任何 JavaScript 开发者都直观易懂赋值操作在底层仍然会被转译为对旧 API 的调用但被类型系统严格约束错误会在编译期暴露。开发者如何使用 Typesafe Updaters使用流程可以概括为三步声明编写一个 updatable query 或 fragment显式指定要命令式更新的数据读取从 store 中读出这些数据得到一个所谓的updatable proxy可更新代理对象修改通过 setter 修改这个 updatable proxy例如updatableData.name Godzilla。第 3 步的赋值动作最终会转译为对旧 API 的调用详见下文源码分析但整个过程有了类型安全保证。什么是 updatable query 或 fragment所谓 updatable query 或 fragment就是带有updatable指令的 query 或 fragment。例如# updatable fragment fragment StoryLikeButton_updatable on Story updatable { likeCount doesViewerLike } # updatable query query NameUpdaterUpdateQuery updatable { viewer { name } }updatable指令会在编译期被 Relay 编译器识别并特殊处理详见下文“编译器如何对待 updatable 操作”一节。关键认知updatable queries / fragments 不会被真正抓取这是 Typesafe Updaters 最核心、也最容易误解的一点。updatable query / fragment 中选择的字段会从服务端抓取吗不会服务端根本不知道 updatable queries 和 fragments 的存在它们的字段永远不会被发送网络请求抓取。即使在普通 query / fragment 中 spread 了一个 updatable fragment该 updatable fragment 所选中的字段也不会作为那次请求的一部分被抓取。updatable 操作本质上只是“对 store 中已有数据的读写描述”而非网络请求描述。如果我想同时抓取并修改某个字段怎么办你需要在普通 query/fragment和updatable query/fragment中分别选择该字段# 1) 在普通 fragment 中抓取字段用于渲染 fragment StoryLikeButton on Story { id likeCount doesViewerLike ...StoryLikeButton_updatable # 2) 同时 spread updatable fragment } # 3) updatable fragment 只描述“要修改哪些字段” fragment StoryLikeButton_updatable on Story updatable { likeCount doesViewerLike }两个 fragment 各自负责各自的职责普通 fragment 负责抓取与渲染updatable fragment 负责允许命令式修改。由此带来的一系列后果FAQ 明确列出了这一设计带来的一系列约束理解它们能避免踩坑读取 updatable 数据时可能缺失当从 store 中读出 updatable 数据时如果该数据当前不在 store 中结果可能是缺失的需要做空值检查不能在 updatable query/fragment 中 spread 普通 fragment普通 fragment 依赖服务端抓取的数据与 updatable 的语义冲突生成的 artifact 不包含 query ID也不包含 normalization ASTnormalization AST 原本用于把网络数据写入 store而 updatable 操作根本不参与网络抓取自然不需要它defer等指令在此上下文中没有意义会被禁止这些指令都服务于网络数据的渐进式交付与纯本地读写场景无关。编译器如何对待 updatable 操作源码佐证上述行为可以从编译器源码中得到印证。在 apply_transforms.rs 中编译管线会对 updatable 操作执行专门的变换。其中 skip_updatable_queries.rs 里的SkipUpdatableQueriesTransform会遍历整个 program凡是带updatable指令的操作定义都会执行Transformed::Delete也就是直接把 updatable query 从生成网络中剔除fn transform_operation(mut self, operation: OperationDefinition) - TransformedOperationDefinition { if operation .directives .iter() .any(|directive| directive.name.item *UPDATABLE_DIRECTIVE) { Transformed::Delete } else { Transformed::Keep } }与此同时annotate_updatable_fragment_spreads.rs 等变换会把 updatable fragment spread 标注为内部指令__updatable。这两点共同说明了 FAQ 所述事实的底层机制updatable 操作不参与网络抓取管线自然也不会产生 query ID 与 normalization AST。updatable proxy 的底层实现运行时入口readUpdatableQuery的运行时实现在 packages/relay-runtime/mutations/readUpdatableQuery.js 中。它从 store 的根记录proxy.getRoot()出发结合 variables 与updatableQuery.fragment.selections调用createUpdatableProxy构建代理对象function readUpdatableQuery(query, variables, proxy, missingFieldHandlers) { const updatableQuery getUpdatableQuery(query); return { updatableData: createUpdatableProxy( proxy.getRoot(), variables, updatableQuery.fragment.selections, proxy, missingFieldHandlers, ), }; }readUpdatableFragment的实现位于 packages/relay-runtime/mutations/readUpdatableFragment.js。它首先通过fragmentReference[ID_KEY]拿到目标记录 id再从 store 中取出对应记录作为代理根同时通过getVariablesFromFragment解析 fragment 变量const fragmentRoot proxy.get(id); invariant(fragmentRoot ! null, No record with ${id} was found. ...);注意源码中的注释复数形式的 fragment references 目前不被支持plural fragment references are currently not supported。getter/setter 如何生成真正生成 updatable proxy 的核心逻辑在 packages/relay-runtime/mutations/createUpdatableProxy.js 中它逐条遍历 selections用Object.defineProperty为每个字段挂上 getter 与 setter标量字段ScalarFieldgetter 调用updatableProxyRootRecord.getValue(...)读取setter 调用setValue__UNSAFE(...)写回。值得注意的是源码中有一个nonUpdatableKeys [id, __id, __typename, js]数组——这些键的 setter 被显式置为undefined也就是说像id、__typename这类元数据字段是不可赋值的关联字段LinkedField单数与复数getter 使用getLinkedRecord/getLinkedRecords并递归为关联记录构建子代理setter 则通过setLinkedRecord/setLinkedRecords建立关联要求传入的对象必须携带__id字段内联 fragmentInlineFragment仅当记录的getType()与selection.type匹配时才递归展开——这正是 FAQ 提到的type refinement类型收窄在底层的体现ClientExtension直接递归展开天然支持 client schema extension 中的字段FragmentSpread被显式忽略其余变体Defer、Stream、RelayResolver等会抛出错误因为它们在 updatable 上下文中没有意义。赋值语义上还有一些值得注意的细节给复数关联字段赋null会抛错提示“应该赋空数组而不是 null”复数关联字段的数组中不允许出现 null 或 undefined 元素关联字段 setter 要求目标记录已存在于 store 中否则抛错Did not find item with data id ... in the store.。当字段缺失时getter 还会尝试调用missingFieldHandlers如getLinkedRecordUsingMissingFieldHandlers、getScalarUsingMissingFieldHandlers来兜底解析缺失数据。在__DEV__环境下生成的代理对象会被Object.freeze冻结帮助尽早发现误用。在哪里拿到store并调用这些 APIFAQ 的 Misc 部分回答了“store从哪里来”这一高频问题。包含readUpdatableQuery和readUpdatableFragment方法的类包括RelayRecordSourceSelectorProxy、RecordSourceProxy与RelayRecordSourceProxy。你可以通过以下途径获取其实例mutation / subscription 的 updater 函数中mutation 的 optimistic updater 中使用RelayModernEnvironment的commitUpdate、applyUpdate等方法时使用独立的commitLocalUpdate方法时。commitLocalUpdate的实现很轻量见 packages/relay-runtime/mutations/commitLocalUpdate.js它只是把环境与 updater 转发给environment.commitUpdate(updater)function commitLocalUpdate(environment, updater) { environment.commitUpdate(updater); }实战示例一在 mutation updater 中初始化客户端字段下面这个完整示例来自 imperatively-modifying-store-data 指南。场景通过 client schema extension 给Feedback类型新增一个is_new_comment字段并在创建 Feedback 的 mutation 完成后将其设为true。先定义 schema extension# Feedback.graphql extend type Feedback { is_new_comment: Boolean }再在 mutation 的updater中通过readUpdatableFragment完成更新// CreateFeedback.js function commitCreateFeedbackMutation(environment, input) { return commitMutation(environment, { mutation: graphql mutation CreateFeedbackMutation($input: FeedbackCreateData!) { feedback_create(input: $input) { feedback { id # Step 1: 在 mutation 响应中 spread updatable fragment ...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: 调用 readUpdatableFragment 得到 updatable proxy const {updatableData} store.readUpdatableFragment( graphql fragment CreateFeedback_updatable_feedback on Feedback updatable { is_new_comment } , feedbackRef, ); // Step 5: 直接给属性赋值 updatableData.is_new_comment true; }, }); }这个例子完整展示了三步走的流程先在 mutation 响应中 spread updatable fragment目的是拿到 fragment reference 并确保记录已写入 store再调用readUpdatableFragment读取代理最后通过 setter 赋值。updater 执行完后所有记录下来的更新会被写入 store所有受影响的组件都会重新渲染。实战示例二在用户交互中切换本地状态再看一个更贴近日常的场景——点击按钮切换is_selected字段同样定义在 client schema extension 中# User.graphql extend type User { is_selected: Boolean }// UserSelectToggle.react.js function UserSelectToggle({userId, viewerRef}) { const viewer useFragment( graphql fragment UserSelectToggle_viewer on Viewer { user(user_id: $user_id) { id name is_selected ...UserSelectToggle_updatable_user } } , viewerRef, ); const environment useRelayEnvironment(); return ( button onClick{() { commitLocalUpdate(environment, (store) { const userRef viewer.user; if (userRef null) { return; } const {updatableData} store.readUpdatableFragment( graphql fragment UserSelectToggle_updatable_user on User updatable { is_selected } , userRef, ); updatableData.is_selected !viewer?.user?.is_selected; }); }} {viewer?.user?.is_selected ? Deselect : Select} {viewer?.user?.name} /button ); }与上一个例子的区别在于commitLocalUpdate的 updater不接受第二个参数没有关联的网络 payload。指南中还提到这个例子可以用environment.commitPayloadAPI 改写但那样会失去类型安全。何时该用readUpdatableQuery而非readUpdatableFragmentreadUpdatableQuery与readUpdatableFragment的核心区别是前者不需要传 fragment reference只需要你从根Query类型到目标记录之间有一条已知的路径。官方指南明确列出推荐使用readUpdatableQuery的几种场景手头没有现成的 fragment reference例如commitLocalUpdate的调用与某个组件并无直接关联拿不到选择“父记录”的 fragment——由于 Relay 存在一个已知的类型空洞known type holeupdatable fragments 不能 spread 在顶层希望在 updatable fragment 中使用变量目前 updatable fragments 会复用传入 query 的变量这意味着你无法让 updatable fragment 拥有 fragment-local 变量也无法多次调用readUpdatableFragment并每次传入不同变量。一个使用readUpdatableQuery的完整例子改写自 imperatively-modifying-store-data 指南// NameUpdater.react.js const onSubmit () { commitLocalUpdate(environment, (store) { const {updatableData} store.readUpdatableQuery( graphql query NameUpdaterUpdateQuery updatable { viewer { name } } , {}, ); const viewer updatableData.viewer; if (viewer ! null) { viewer.name newName; } }); };注意这里通过readUpdatableQuery直接以{}作为 variables 调用无需任何 fragment referenceupdatableData.viewer仍是一个可为空的代理对象需要空值检查后再赋值。使用建议与边界总结综合 FAQ、指南与源码可以把 Typesafe Updaters 的正确使用姿势归纳为以下几点用途命令式修改 store 中的本地数据尤其适合 client schema extensions 字段的初始化与更新、复杂客户端更新以及invalidateStore、删除节点、查找连接等只有 updater 才能做到的操作不要用它触发副作用需要触发副作用时请使用onCompleted回调——它保证只调用一次而 updater / optimistic updater 可能被重复调用读写分离要展示的数据用普通 query/fragment 抓取要修改的数据在 updatable query/fragment 中声明两者各自选择所需字段注意空值由于 updatable 数据依赖 store 中已有的记录读取结果可能缺失务必做空值检查或用required指令理解执行时机optimistic updater 在 mutation 触发时执行、完成或失败后回滚普通 updater 在 mutation 成功完成后执行。若两个 optimistic response 都修改同一值第一个回滚时第二个不会被重新计算该值会保持“叠加后”的结果。如果你还想了解更底层的RecordProxy、RecordSourceProxy方法如setValue、setLinkedRecord等可以进一步阅读 store API 参考 与旧式命令式更新指南 imperatively-modifying-store-data-legacy.md对比新旧两套 API 的差异后你会更深刻地体会 Typesafe Updaters 在类型安全与开发体验上的改进。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay Typesafe Updaters 全面指南用 readUpdatableQuery 与 readUpdatableFragment 类型安全地命令式更新 Store 数据Relay Typesafe Updaters 全面指南用 readUpdatableQuery 与 readUpdatableFragment 类型安全地命前端开发工具Adblock Fast Chrome扩展开发从零开始构建浏览器广告拦截器Adblock Fast Chrome扩展开发从零开始构建浏览器广告拦截器 Adblock Fast是一款适用于Windows、Android、iOS、Chr前端开发工具Relay 命令式修改 Store 数据readUpdatableFragment 与 readUpdatableQuery 类型安全 Updater 实战指南Relay 命令式修改 Store 数据 readUpdatableFragment 与 readUpdatableQuery 类型安全 Updater 实战前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

django CMS 2.2 升级指南:依赖重构、权限增强与向后不兼容变更全解析

django CMS 2.2 升级指南:依赖重构、权限增强与向后不兼容变更全解析

CMS后端 【免费下载链接】django-cms The easy-to-use and developer-friendly enterprise CMS powered by Django 项目地址: https://gitcode.com/gh_mirrors/dj/django-cms 点击查看 免费下载 django CMS 2.2 是一次以"工程化收敛"为核心的版本升级&a…

2026/9/24 16:34:42 阅读更多 →
AI用88小时解开90年难题,人类只讨论了14天

AI用88小时解开90年难题,人类只讨论了14天

2026年9月8日,OpenAI 发布声明说,他们的一个内部模型解开了纳维-斯托克斯问题,一道数学界悬了90多年的题。一万多个AI智能体,88小时,165页论文。就在这个声明公布的两分钟前,一位澳大利亚数学家在自己的社交…

2026/9/24 16:34:42 阅读更多 →
Altair Lookup 变换(transform_lookup)完整指南:在图表内实现数据关联与地理可视化增强

Altair Lookup 变换(transform_lookup)完整指南:在图表内实现数据关联与地理可视化增强

数据可视化 【免费下载链接】altair Declarative visualization library for Python 项目地址: https://gitcode.com/gh_mirrors/al/altair 点击查看 免费下载 本文围绕 Altair 中的 Lookup 变换(Chart.transform_lookup / LookupTransform)…

2026/9/24 16:34:42 阅读更多 →

最新新闻

如何在本地跑起 fragments:开源 AI 应用生成器完整配置指南(Next.js 模板)

如何在本地跑起 fragments:开源 AI 应用生成器完整配置指南(Next.js 模板)

如何在本地跑起 fragments:开源 AI 应用生成器完整配置指南(Next.js 模板) 【免费下载链接】fragments Open-source Next.js template for building apps that are fully generated by AI. By E2B. 项目地址: https://gitcode.com/GitHub_T…

2026/9/24 17:24:29 阅读更多 →
FunClip:3 步完成 AI 视频智能剪辑

FunClip:3 步完成 AI 视频智能剪辑

FunClip:3 步完成 AI 视频智能剪辑 【免费下载链接】FunClip FunASR-powered video transcription, subtitle generation, and LLM-assisted clipping tool with a local Gradio UI. 项目地址: https://gitcode.com/GitHub_Trending/fu/FunClip 两小时会议录…

2026/9/24 17:24:29 阅读更多 →
850kbps 离线文件传输:libcimbar 把屏幕和摄像头变成信道

850kbps 离线文件传输:libcimbar 把屏幕和摄像头变成信道

850kbps 离线文件传输:libcimbar 把屏幕和摄像头变成信道 【免费下载链接】libcimbar Optimized implementation for color-icon-matrix barcodes 项目地址: https://gitcode.com/GitHub_Trending/li/libcimbar libcimbar 是一款做离线文件传输的 C 库&#…

2026/9/24 17:24:28 阅读更多 →
GPU训练脚本迁移昇腾NPU只需5步简单修改:TorchNPU模型迁移实战指南

GPU训练脚本迁移昇腾NPU只需5步简单修改:TorchNPU模型迁移实战指南

GPU训练脚本迁移昇腾NPU只需5步简单修改:TorchNPU模型迁移实战指南 【免费下载链接】pytorch 作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU&#xff0c…

2026/9/24 17:24:28 阅读更多 →
详解 JWT

详解 JWT

什么是 JWTJWT(JSON Web Token):是一种无状态、自包含的身份令牌,用字符串传递用户身份信息,不需要服务端保存会话记录。JWT 整体格式三段用.分隔:Header.Payload.Signature1.Header 头部记录加密签名算法&…

2026/9/24 17:24:28 阅读更多 →
基于 Java Spring Boot 的灾害应急救援平台设计与实现

基于 Java Spring Boot 的灾害应急救援平台设计与实现

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 1. 引言 随着自然灾害和突发公共事件的频发,传统应急救援模式在信息传递、资源调度和协同指挥等方面暴露出响应慢、信息孤岛、资源调配不透明等问题。本文基…

2026/9/24 17:23:28 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →