ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

wp-calypso React Query 迁移实战:Mutation(增删改)的完整实现与最佳实践

2026/9/23 2:47:26 拓冰建站 浏览量
wp-calypso React Query 迁移实战:Mutation(增删改)的完整实现与最佳实践 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),仅供参考