wp-calypso React Query 迁移实战Mutation增删改的完整实现与最佳实践【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso本指南基于 wp-calypso 仓库中.claude/skills/calypso-react-query-migration/系列技能文档聚焦 React Query 迁移中 Mutations创建 / 更新 / 删除部分的完整落地流程。你将掌握从api-core的 HTTP 请求层、api-queries的mutationOptions封装到消费组件调用与 Redux 清理的六步实战方法并理解乐观更新optimistic updates与缓存失效的底层原理。文中所有示例均可在 packages/api-queries 与 packages/api-core 中找到真实实现作为参考。Mutations 与 Queries 的本质区别为什么不需要 Bridge 组件在 Calypso 的 React Query 迁移方案中读取类查询Queries通常需要一层 Bridge 组件来把查询数据接入旧有的 Redux 数据流而Mutations 走的是完全不同的路径它们遵循与 Queries 相同的包布局api-core放 HTTP 请求、api-queries放mutationOptionsuseMutation但不需要 Bridge 组件——消费组件直接调用mutate()即可。核心差异在于查询结果往往被多个组件共享、需要经过 Redux 中转而一次 mutation 的触发者是明确的——某个组件里的按钮或表单。因此 mutation 的调用方consumer天然就是副作用提示、跳转的执行者这让数据流更短、更直接。整体迁移分六步在api-core中添加 mutatorHTTP 请求函数在api-queries中添加mutationOptions工厂含缓存失效逻辑优先实现乐观更新更新消费组件useMutationmutate()在同一提交中清理 Redux编写测试第 1 步在 api-core 中添加 MutatorMutations 的 HTTP 层与 queries 的 fetchers 放在同一目录。新建packages/api-core/src/read-{name}/mutators.ts使用包内统一的wpcomfetcher 发起请求import { wpcom } from ../wpcom-fetcher; import type { CreateXxxParams, XxxResponse } from ./types; export const createXxx ( params: CreateXxxParams ): Promise XxxResponse { return wpcom.req.post( { path: /read/xxx/new, apiVersion: 1.2, body: params, } ); }; export const updateXxx ( params: UpdateXxxParams ): Promise XxxResponse { return wpcom.req.post( { path: /read/xxx/${ params.owner }/${ params.slug }/update, apiVersion: 1.2, body: params, } ); }; export const deleteXxx ( owner: string, slug: string ): Promise void { return wpcom.req.post( { path: /read/xxx/${ owner }/${ slug }/delete, apiVersion: 1.2, body: {}, } ); };然后从目录的index.ts统一导出export * from ./mutators;真实实现参照read-lists 的 mutators仓库中 packages/api-core/src/read-lists/mutators.ts 就是这一模式的生产级实现。注意它与模板示例有两个实战差异路径参数必须做 URL 编码owner和slug都是用户可控的字符串可能含空格、中文或特殊字符真实代码用encodeURIComponent包裹export const updateReadList ( list: UpdateReadListParams ): Promise ReadListResponse { return wpcom.req.post( { path: /read/lists/${ encodeURIComponent( list.owner ) }/${ encodeURIComponent( list.slug ) }/update, apiVersion: 1.2, }, list ); };请求体作为第二个参数传入wpcom.req.post( { path, apiVersion }, body )而不是放进第一个对象的body字段。这两种写法都可用但仓库现状统一采用“配置对象 body 参数”的形态新代码应保持一致。同文件还展示了 follow / unfollow 这类“状态翻转型”mutation 的写法——它们也走POST请求体为空对象{}export const followReadList ( owner: string, slug: string ): Promise ReadListResponse { return wpcom.req.post( { path: /read/lists/${ encodeURIComponent( owner ) }/${ encodeURIComponent( slug ) }/follow, apiVersion: 1.2, }, {} ); };对应的参数与返回类型定义在 packages/api-core/src/read-lists/types.tsexport interface CreateReadListParams { title: string; description?: string; is_public?: boolean; } export interface UpdateReadListParams { title: string; slug: string; owner: string; description?: string; is_public?: boolean; } export interface ReadListResponse { list: ReadList; }类型约定mutators 的入参类型CreateXxxParams、UpdateXxxParams与响应类型XxxResponse都定义在types.ts中与 fetchers 共享避免重复声明。第 2 步在 api-queries 中添加 Mutation 工厂api-queries层负责把 HTTP 函数包装成 TanStack React Query 的mutationOptions并在其中集中处理缓存失效。新建packages/api-queries/src/read-{name}.tsimport { createXxx, deleteXxx, updateXxx } from automattic/api-core; import { mutationOptions } from tanstack/react-query; import { queryClient } from ./query-client; export const createXxxMutation () mutationOptions( { mutationFn: createXxx, onSuccess: () { queryClient.invalidateQueries( { queryKey: readXxxListQuery().queryKey } ); }, } ); export const updateXxxMutation () mutationOptions( { mutationFn: updateXxx, onSuccess: ( data ) { queryClient.invalidateQueries( { queryKey: readXxxQuery( data.owner, data.slug ).queryKey } ); queryClient.invalidateQueries( { queryKey: readXxxListQuery().queryKey } ); }, } ); export const deleteXxxMutation () mutationOptions( { mutationFn: ( { owner, slug }: { owner: string; slug: string } ) deleteXxx( owner, slug ), onSuccess: ( _data, { owner, slug } ) { queryClient.removeQueries( { queryKey: readXxxQuery( owner, slug ).queryKey } ); queryClient.invalidateQueries( { queryKey: readXxxListQuery().queryKey } ); }, } );缓存失效规则务必逐条遵守Mutation 类型缓存操作原因CreateinvalidateQueries列表查询让新条目出现在列表中UpdateinvalidateQueries条目查询且列表查询详情与列表都要反映新值Delete条目查询用removeQueries列表查询用invalidateQueries条目已不存在移除可避免后续误用陈旧数据Follow / Unfollow失效所有读取关注状态的查询关注布尔值被多处 UI 读取跳过缓存失效是最常见的 bug——UI 会一直显示陈旧数据直到用户手动刷新页面。真实实现参照read-lists 的 mutation 工厂仓库中 packages/api-queries/src/read-lists.ts 是模板的完整落地版但有一个关键差异值得注意Calypso 启动的是自己的 QueryClient见client/state/query-client.ts而不是本包的单例因此注释明确指出见 read-lists.ts每个 mutation 工厂都接受调用方的QueryClient参数并在消费组件中通过useQueryClient()传入export const deleteReadListMutation ( queryClient: QueryClient ) mutationOptions( { meta: { statId: read-list-delete }, mutationFn: ( { owner, slug }: { owner: string; slug: string } ) deleteReadList( owner, slug ), onSuccess: ( _data, { owner, slug } ) { queryClient.removeQueries( { queryKey: readListQuery( owner, slug ).queryKey } ); return invalidateSubscribedLists( queryClient ); }, } );updateReadListMutation则在onSuccess中直接setQueryData把服务端返回的最新数据写回条目缓存再失效列表缓存减少一次不必要的 refetchexport const updateReadListMutation ( queryClient: QueryClient ) mutationOptions( { meta: { statId: read-list-update }, mutationFn: updateReadList, onSuccess: ( data ) { queryClient.setQueryData( readListQuery( data.list.owner, data.list.slug ).queryKey, data ); return invalidateSubscribedLists( queryClient ); }, } );meta 与埋点statId 的注入方式从真实代码可以看到每个 mutation 都带meta: { statId: read-list-create }之类的字段。这个meta的类型是在 packages/api-queries/src/query-client.ts 中通过模块扩展module augmentation声明的export interface ApiQueriesMutationMeta extends Record string, unknown { statId?: string; } declare module tanstack/react-query { interface Register { mutationMeta: ApiQueriesMutationMeta; queryMeta: ApiQueriesQueryMeta; } }该文件的注释说明了一个重要的类型细节TanStack 的Register全局只允许声明一次所以 Calypso 用接口扩展的方式而非直接改 TanStack 的声明开放给各 app 追加自己的 meta并且必须继承Record string, unknown 否则 TypeScript 会因接口缺少隐式索引签名导致meta读取静默退化为{}。新增 mutation 时建议沿用meta.statId约定便于统一埋点统计。第 3 步优先使用乐观更新Optimistic Updates这是迁移中最容易造成 UX 回退的一步也是文档强调最多的地方。为什么必须做乐观更新旧的 Redux >export const updateXxxOptimisticMutation () mutationOptions( { mutationFn: updateXxx, onMutate: async ( newValue ) { // 取消进行中的 refetch避免它们覆盖我们的乐观写入 await queryClient.cancelQueries( { queryKey: readXxxQuery( newValue.id ).queryKey } ); // 快照旧值用于失败回滚 const previous queryClient.getQueryData( readXxxQuery( newValue.id ).queryKey ); // 乐观写入新值 queryClient.setQueryData( readXxxQuery( newValue.id ).queryKey, newValue ); return { previous }; }, onError: ( _err, variables, context ) { if ( context?.previous ) { queryClient.setQueryData( readXxxQuery( variables.id ).queryKey, context.previous ); } }, onSettled: ( _data, _err, variables ) { // 无论成功失败都 refetch 与服务端对齐 queryClient.invalidateQueries( { queryKey: readXxxQuery( variables.id ).queryKey } ); }, } );参考实现userPreferenceOptimisticMutation文档明确指出的参考实现位于 packages/api-queries/src/me-preferences.ts。这个生产级实现完整演示了onMutate → onError 回滚的骨架且由于用户偏好是“整体对象”结构它用函数式setQueryData做局部合并mergePreferences负责在 preference 被置空时从旧数据中删除该键export const userPreferenceOptimisticMutation P extends keyof UserPreferences ( preferenceName: P ) mutationOptions( { meta: { statId: getUserPreferenceMutationStatId( user-pref-opt-update, preferenceName ), }, mutationFn: userPreferenceMutation( preferenceName ).mutationFn, onMutate: async ( value ) { await queryClient.cancelQueries( { queryKey: rawUserPreferencesQuery().queryKey } ); const previous queryClient.getQueryData( rawUserPreferencesQuery().queryKey ); const newData { [ preferenceName ]: value } as UserPreferences; queryClient.setQueryData( rawUserPreferencesQuery().queryKey, ( oldData ) { return mergePreferences( oldData, preferenceName, newData ); } ); return { previous }; }, onError: ( _err, _variables, context ) { if ( context?.previous ) { queryClient.setQueryData( rawUserPreferencesQuery().queryKey, context.previous ); } }, } );不同 mutation 类型的乐观模式Create乐观地把新条目 push 进列表缓存真实 ID 由服务端生成时在onSuccess中用临时 ID 替换真实 IDUpdate快照条目 → 写入新值 → 出错时回滚快照Delete乐观地从列表缓存移除条目出错时恢复Follow / Unfollow乐观翻转缓存中的布尔值出错时回滚什么时候不要用乐观更新结果依赖服务端计算、客户端无法预测的数据如服务端生成的 slug、服务端分配的 ID、派生计数等。此时应使用占位placeholderonSuccess中对账或者放弃乐观更新、接受延迟罕见或非交互式 mutation如后台同步、管理操作额外的复杂度不值得永远记得与onSettled失效配套使用——这样无论成功失败缓存最终都会对齐服务端真相。第 4 步更新消费组件在组件层一切变得直接调用useMutation( mutationOptionsFactory() )拿到mutate与isPending把副作用跳转、提示挂在mutate的第二个参数上import { deleteXxxMutation } from automattic/api-queries; import { useMutation } from tanstack/react-query; import page from automattic/calypso-router; import { errorNotice, successNotice } from calypso/state/notices/actions; function MyComponent( { item } ) { const dispatch useDispatch(); const translate useTranslate(); const { mutate: deleteItem, isPending } useMutation( deleteXxxMutation() ); const handleDelete () { deleteItem( { owner: item.owner, slug: item.slug }, { onSuccess: () { page( /reader ); dispatch( successNotice( translate( Deleted successfully. ) ) ); }, onError: () { dispatch( errorNotice( translate( Unable to delete. ) ) ); }, } ); }; return Button onClick{ handleDelete } disabled{ isPending } /; }副作用归属的职责划分这是让 mutation 保持可复用reusable的关键约定提示notices、导航、Redux receive action放在消费组件的onSuccess/onError回调里缓存失效放在api-queries 层mutation 的onSuccess里这样同一个deleteXxxMutation可以被多个页面复用而每个消费组件可以各自控制自己的 UX有的删除后跳转/reader有的留在原地只弹提示。真实项目参考packages/api-queries/src/read-follows.ts 中followSiteMutation/unfollowSiteMutation展示了“关注”类 mutation 的完整形态成功后在onSuccess里用patchSiteSubscription乐观修补订阅缓存再失效订阅列表查询同时保留meta.statId埋点。第 5 步在同一提交中清理 ReduxMutations 不需要 Bridge 模式——一旦消费组件改用isPendingRedux 里就没有任何东西再读取isCreating/isUpdating状态了。此时应当在同一个提交里删除以下内容READER_XXX_CREATE/_UPDATE/_DELETEaction types请求 action creator如createReaderList、updateReaderList、deleteReaderListData-layer handlersisCreatingXxx/isUpdatingXxxreducers 与 selectors但要保留RECEIVEactions 与对应 reducers——如果还有其他组件仍然从 Redux 读取数据就在消费组件的onSuccess中 dispatch 它们。这样能保证迁移期间新旧数据流并存、互不破坏。第 6 步测试 Mutationmutation 的测试重点是 mock 正确的 HTTP 方法和路径。删除类 mutation 的 nock 示例nock( https://public-api.wordpress.com ) .post( /rest/v1.2/read/xxx/owner/slug/delete ) .reply( 200 );注意路径格式wpcom.req.post( { path: /read/xxx/..., apiVersion: 1.2 } )对应到 public-api 的完整 URL 是https://public-api.wordpress.com/rest/v1.2/read/xxx/...——nock 的路径要把apiVersion拼进 URL 中。测试检查清单用正确的方法 mocknock.post()对应 POST其余方法同理验证成功路径notice 触发、导航发生、查询被失效验证错误路径错误 notice 触发验证isPending状态会禁用触发按钮防止重复提交总结Mutations 迁移的关键心法回到开头的问题——为什么 mutations 不需要 Bridge 组件因为mutation 的调用方天然拥有完整的上下文它知道自己在改什么、改完要去哪、要给用户什么反馈。React Query 的mutationOptionsuseMutation把“网络请求、缓存管理、加载状态”收拢到 api-queries 层把“UX 副作用”留给组件层形成一条清晰的责任链。迁移时请始终牢记三条主线缓存失效不可跳过否则 UI 陈旧、用户可见的变更默认乐观更新否则体验回退、Redux 清理与迁移同提交否则新旧状态双轨并存。遵循本指南的六步流程配合仓库中 me-preferences.ts、read-lists.ts 等生产级参考实现即可把 Calypso 中任意一个增删改功能平滑迁移到 React Query。【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址: https://gitcode.com/gh_mirrors/wp/wp-calypso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
发offer前必看的5个新手避坑指南 发offer前必看的5个新手避坑指南 凌晨两点,你盯着屏幕上的红色报错信息,心里只剩一个念头:这代码到底怎么就挂了?Stack Trace 长得像天书,从最底层的 NullPointerException 到最外层的… · 2026/9/23 2:47:17
2026最新手机全息投影性能优化:API大改后如何稳住60帧 2026最新手机全息投影性能优化:API大改后如何稳住60帧 刚把项目从旧版迁移到2026最新的渲染管线,是不是感觉脑子嗡嗡的? 之前的API调用逻辑全乱了,原本跑得飞快的渲染循环,现在稍微加点特效就掉帧。 别慌,这就是典型的版本升级后… · 2026/9/23 2:47:10
dio_web_adapter 实战指南:让 Dio 在 Web 与 WASM 上无缝运行 dio_web_adapter 实战指南:让 Dio 在 Web 与 WASM 上无缝运行 【免费下载链接】dio A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading,… · 2026/9/23 2:47:09
PowerSploit Recon 模块 Get-DomainDFSShare 深度解析:枚举域内分布式文件系统共享 PowerSploit Recon 模块 Get-DomainDFSShare 深度解析:枚举域内分布式文件系统共享 【免费下载链接】PowerSploit PowerSploit - A PowerShell Post-Exploitation Framework 项目地址: https://gitcode.com/gh_mirrors/po/PowerSploit
导读
Get-DomainDFSSh… · 2026/9/23 3:35:02
YOLO遥感油罐检测数据集全解析:标签格式转换与训练避坑指南 简介:面向YOLO目标检测学习者与遥感图像分析人员,这份遥感油罐检测数据集来自真实场景,图片质量高、场景丰富,使用LabelImg标注且框体质量高,可直接用于YOLOv5、YOLOv8等主流目标检测模型的训练与算法效果验证。压缩包… · 2026/9/23 3:35:02
3步搞定ngg入门到精通:告别报错一脸懵 3步搞定ngg入门到精通:告别报错一脸懵 凌晨三点,屏幕上的红色报错代码像鬼影一样在跳动。你盯着那个 StackTrace ,感觉脑子里一片空白,甚至想直接拔电源。别慌,这种“报错一堆看不懂… · 2026/9/23 3:34:56
Kimi收入20天超全年:K3模型、订阅制与AI应用爆发信号 1. 一条出售新闻和一组热词,透露了AI赛道的资金流向信号1.1 沐瞳交易背后的“聚焦主业”逻辑这条新闻刚出来的时候,我身边不少做游戏和做AI的朋友都在转发。字节跳动以60亿美元的价格出售沐瞳科技,这个数字本身就很有话题性。沐瞳科技是《Mob… · 2026/9/23 3:34:56
图解原理搞懂安卓优化,3步解决卡顿,拒绝只会抄代码 图解原理搞懂安卓优化,3步解决卡顿,拒绝只会抄代码 是不是刷爆了B站和掘金,看了一堆教程还是不会写项目?那些“高斯模糊”、“Shader加速”的视频看得你热血沸腾,一动手写原生Android应用,列表一长就掉帧,点击一下UI卡得像PPT。别… · 2026/9/23 3:34:56
投研领域的Claude Code:Harness如何重构金融工作流 如果你关注AI编程工具,大概率听说过Claude Code这个名字。它让开发者用自然语言直接指挥一个智能体完成从读代码、改代码到跑测试、提PR的完整闭环。那如果把这个思路从代码世界搬到金融市场,会是什么样?李昱琦创办的Panda AI正在做这件事——… · 2026/9/23 3:34:56
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29