Refine 中用 useUpdate 钩子更新记录:从实现 DataProvider.update 到乐观更新与查询失效机制
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本文基于 Refine 官方教程中“Updating a Record”一节,完整演示如何在数据提供层实现update方法、如何用useUpdate钩子更新单条 API 记录,并结合packages/core源码剖析该钩子背后的乐观更新、智能失效(Smart Invalidations)与可撤销(undoable)变更机制。读完本文,你不仅能复刻教程中的完整操作,还能理解useUpdate在 TanStack Query 之上封装了哪些自动化行为。
一、更新记录的两步走:update方法与useUpdate钩子
Refine 的数据交互遵循“数据提供层 + 数据钩子”的分层模型。要更新一条记录,需要完成两件事:
- 在 DataProvider 中实现
update方法——这是所有更新操作的统一入口; - 在组件中调用
useUpdate钩子——它会最终路由到 DataProvider 的update方法。
useUpdate钩子及全部数据钩子的源码位于 useUpdate.ts,其 JSDoc 明确说明它是 “react-query'suseMutation的修改版,专用于 update 变更”,并以传入<Refine>的 DataProvider 的update方法作为变更函数(mutation function)。
二、实现update方法
update方法接受四个属性:
| 属性 | 说明 |
|---|---|
resource | 要更新的实体(资源名) |
id | 要更新的记录 ID |
variables | 发送到 API 的数据对象 |
meta | 传递给钩子的附加数据(如筛选、分页等查询上下文) |
Refine 自带的 fake API(https://api.fake-rest.refine.dev)要求通过PATCH /products/:id端点更新记录。因此我们在>import type { DataProvider } from "@refinedev/core"; const API_URL = "https://api.fake-rest.refine.dev"; export const dataProvider: DataProvider = { getOne: async ({ resource, id, meta }) => { const response = await fetch(`${API_URL}/${resource}/${id}`); if (response.status < 200 || response.status > 299) throw response; const data = await response.json(); return { data }; }, update: async ({ resource, id, variables }) => { const response = await fetch(`${API_URL}/${resource}/${id}`, { method: "PATCH", body: JSON.stringify(variables), headers: { "Content-Type": "application/json", }, }); if (response.status < 200 || response.status > 299) throw response; const data = await response.json(); return { data }; }, getList: () => { throw new Error("Not implemented"); }, /* ... */ };
从源码结构看,DataProvider 接口中update是必选方法(与可选的updateMany?、getMany?不同),签名为:
update: <TData extends BaseRecord = BaseRecord, TVariables = {}>( params: UpdateParams<TVariables>, ) => Promise<UpdateResponse<TData>>;即update返回Promise<UpdateResponse<TData>>,其中UpdateResponse定义为{ data?: TData }。这也解释了教程代码为什么统一用return { data }包裹响应体——这是钩子约定读取结果的方式。
三、使用useUpdate钩子
实现update方法后,就可以调用useUpdate更新单条记录。教程中我们新建EditProduct组件:先用useOne拉取待更新记录,再用useUpdate发起更新(src/pages/products/edit.tsx):
import { useOne, useUpdate } from "@refinedev/core"; export const EditProduct = () => { const { result, query: { isLoading }, } = useOne({ resource: "products", id: 123 }); const { mutate, mutation: { isPending: isUpdating }, } = useUpdate(); if (isLoading) { return <div>Loading...</div>; } const updatePrice = async () => { await mutate({ resource: "products", id: 123, values: { price: Math.floor(Math.random() * 100), }, }); }; return ( <div> <div>Product name: {result?.name}</div> <div>Product price: ${result?.price}</div> <button onClick={updatePrice}>Update Price</button> </div> ); };然后在src/App.tsx中把EditProduct挂到<Refine />组件内:
import { Refine } from "@refinedev/core"; import { dataProvider } from "./providers/data-provider"; import { ShowProduct } from "./pages/products/show"; import { EditProduct } from "./pages/products/edit"; export default function App(): JSX.Element { return ( <Refine dataProvider={dataProvider}> {/* <ShowProduct /> */} <EditProduct /> </Refine> ); }此时页面会显示产品名称与价格,点击 “Update Price” 按钮后价格即被更新。useUpdate的入参类型UpdateParams中,除教程用到的resource/id/values外还支持mutationMode、undoableTimeout、onCancel、meta、dataProviderName、invalidates、optimisticUpdateMap等选项,这些将在下一节结合源码展开。
四、源码深潜:useUpdate到底做了什么
useUpdate.ts 中,useUpdate内部构建了一个 TanStack Query 的useMutation,并分四个阶段工作:
4.1 mutationFn:参数校验与 DataProvider 调用
在 mutationFn 中,id、values、resourceName三者缺一即抛出预定义错误(missingIdError、missingValuesError、missingResourceError)。随后钩子把values重命名为variables,调用 DataProvider:
dataProvider(...).update<TData, TVariables>({ resource: resource.name, id, variables: values, meta: combinedMeta, });注意resource会先经过select(resourceName)解析——这意味着传入identifier(资源标识符)而非name(API 资源名)时也能正确路由到对应 DataProvider,测试 useUpdate.spec.tsx 中专门有 “when passingidentifierinstead ofname” 的 describe 块验证这一点。
4.2 onMutate:默认开启的乐观更新
这是教程没有展开、但源码里最核心的部分。在 onMutate 阶段:
- 先通过
queryClient.getQueriesData快照保存该资源所有相关查询的当前数据(previousQueries); - 取消正在进行的请求(
cancelQueries); - 只要 mutation 模式不是
pessimistic,就按optimisticUpdateMap配置把values直接合并进缓存——默认配置为{ list: true, many: true, detail: true },即列表查询(list)、多记录查询(many)、详情查询(one)全部即时更新:
if (optimisticUpdateMap.list) { queryClient.setQueriesData({ queryKey: resourceKeys.action("list")... }, (previous) => { // 将 values 合并到 previous.data 中 id 匹配的记录上 }); }因此界面在 API 返回前就显示新值。optimisticUpdateMap的每一项都可以传自定义映射函数OptimisticUpdateMapType(如(previous, values, id) => 新数据),也可以传false关闭对应通道的乐观更新——这些行为在 useUpdate.spec.tsx 的 “when passingoptimisticUpdateMap” 测试块中有完整断言。
4.3 onSettled:智能失效(Smart Invalidations)
教程提示框中提到“用useUpdate更新价格后,之前调用的useOne会自动失效”。源码依据就在 onSettled:
const { invalidates = invalidatesFromProps ?? ["list", "many", "detail"] } = variables; invalidateStore({ resource: identifier, dataProviderName: ..., invalidates, id, });无论成功或失败,钩子都会失效该资源下list/many/detail三类查询(可通过invalidates参数收窄范围),触发 TanStack Query 重新拉取,从而保证屏幕数据始终是最新的——这正是教程中“Smart Invalidations”提示的底层实现。
4.4 onSuccess 与 onError:发布事件、审计日志与回滚
- onSuccess(L467-L563):除了弹出默认的 “Successfully updated xxx” 成功通知,还会通过
publish向resources/{resource}频道发布updated实时事件(供 LiveProvider 使用),并调用log.mutate记录变更前后的数据(供审计日志使用)。onSuccess里还会从旧缓存中抽取被修改字段的原值,作为previousData一并写入日志。 - onError(L564-L613):若存在
onMutate保存的previousQueries快照,会把每个查询的数据逐条恢复为变更前状态;若是mutationCancelled(undoable 模式被用户取消)则静默处理,否则弹出 “Error when updating xxx (status code: ...)” 错误通知。
测试用例 “should work with optimistic update” 完整印证了这条链路:先用mutationMode: "optimistic"发起更新,断言useOne/useList/useMany的结果立刻变为新标题;再让 API 以 500ms 延迟后报错,断言三个查询的结果回滚为初始标题(见 useUpdate.spec.tsx)。
五、三种变更模式(Mutation Mode)
源码中mutationMode支持三种取值,缺省时从<Refine>的 mutation 上下文(useMutationMode)继承:
| 模式 | 行为 |
|---|---|
optimistic(默认) | 请求发出前立即用新值更新缓存,失败后回滚 |
pessimistic | 等待 API 成功响应后才失效并重取,缓存不会被提前修改 |
undoable | 不立即请求,而是把变更加入可撤销队列;在undoableTimeout毫秒内用户可取消(onCancel拿到cancelMutation函数),超时后自动执行真实请求 |
undoable的实现见 mutationFn 中的 updatePromise:它把真实请求包装成doMutation,连同cancelMutation一起通过notificationDispatch分发给通知层,从而在 UI 上呈现带撤销按钮的提示。测试文件中对应 “should work with undoable update” 用例。
六、钩子返回值与错误边界
useUpdate最终返回(L655-L660):
return { mutation: mutationResult, // 完整的 react-query useMutation 结果(isPending/isError/isSuccess...) mutate: handleMutation, // variables 参数可省略的 mutate mutateAsync: handleMutateAsync, overtime: { elapsedTime }, // 加载超时计时(useLoadingOvertime) };mutate/mutateAsync被包装了一层,使得resource/id/values既可以在钩子入参中给,也可以在mutate()调用时给(“with props” 与 “with params” 两种用法在 useUpdate.spec.tsx 中各有一整组 describe 块验证);overtime.elapsedTime配合overtimeOptions可用于实现“加载超过 N 秒就提醒用户”的体验;- 缺失参数时抛出的错误信息(如 “[useUpdate]:
idis not defined but is required in edit and clone actions”)对排查“为什么我的 update 没生效”非常有用,注意id: 0、id: ""等合法值不会误报——测试 “should not throw error when id=''" 专门覆盖了这一边界。
七、小结
这篇教程展示了 Refine 更新记录的完整闭环:DataProvider 的update方法负责把resource/id/variables/meta翻译成一次PATCH请求,useUpdate钩子则在其上叠加了参数校验、乐观更新、失败回滚、跨查询智能失效、实时事件发布与审计日志记录。理解 packages/core/src/hooks/data/useUpdate.ts 与 packages/core/src/hooks/data/useUpdate.spec.tsx 这两份文件后,再遇到useList、useCreate、useDelete等兄弟钩子(同目录 hooks/data),你会看到几乎一致的架构:mutationFn+onMutate乐观更新 +onSettled失效 +onSuccess/onError副作用,只是查询键与数据形状不同。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考