tRPC setData钩子无法满足乐观UI更新的完整数据类型要求
解决tRPC + React Query乐观UI更新的类型安全问题
问题描述
我正在用tRPC和React Query构建mutation,实现新增条目时的乐观UI更新,但遇到了类型约束问题:待更新的查询返回的类型包含许多自动生成的属性(如ID)或数据库关联数据(如tags),但我不想在onMutate里创建大量临时数据来满足这些类型要求。
我的onMutate函数代码:
onMutate: (data) => { const previousJobs = utils.jobs.getAll.getData(); if (previousJobs) { utils.jobs.getAll.setData(undefined, [...previousJobs, { ...data, status: JobStatus.OPEN, priority: JobPriority.NO_PRIORITY, id: "temp", created_at: new Date(), updated_at: new Date(), team_id: "temp", user_id: "temp" }] ); } },
遇到的类型错误:
Type '{ status: "OPEN"; priority: "NO_PRIORITY"; id: string; created_at: Date; updated_at: Date; team_id: string; user_id: string; salary: number; title: string; office_type: string; description: string; due_date: Date; }' is missing the following properties from type '{ user: User & { view: { state: ViewState; }[]; }; _count: { tags: number; candidates: number; }; tags: { id: string; value: string; color: string; }[]; }'
类型安全的解决方法
方法1:补全最小化占位符(快速直接)
既然这些关联属性在乐观更新的UI中可能暂时不需要展示,可以给它们设置符合类型要求的最小占位值,既满足类型约束又不用写冗余数据:
import type { Job } from '@/server/trpc/routers/jobs'; // 导入你的Job类型 onMutate: (data) => { const previousJobs = utils.jobs.getAll.getData(); if (previousJobs) { // 构造符合Job类型的乐观更新对象 const optimisticJob: Job = { ...data, status: JobStatus.OPEN, priority: JobPriority.NO_PRIORITY, id: "temp", created_at: new Date(), updated_at: new Date(), team_id: "temp", user_id: "temp", // 补全缺失的关联属性的最小占位 user: {} as Job['user'], // 若UI不需要展示用户数据,用空对象加类型断言 _count: { tags: 0, candidates: 0 }, tags: [] }; utils.jobs.getAll.setData(undefined, [...previousJobs, optimisticJob]); } },
方法2:用TypeScript类型工具简化定义
通过Omit和Partial创建乐观更新专用类型,复用已有类型的同时简化临时数据的构造:
import type { Job } from '@/server/trpc/routers/jobs'; // 定义乐观更新类型:省略自动生成/关联属性,再补充必填的临时值 type OptimisticJob = Omit<Job, 'user' | '_count' | 'tags'> & { user?: Partial<Job['user']>; _count?: Partial<Job['_count']>; tags?: Job['tags']; }; onMutate: (data) => { const previousJobs = utils.jobs.getAll.getData(); if (previousJobs) { const optimisticJob: OptimisticJob = { ...data, status: JobStatus.OPEN, priority: JobPriority.NO_PRIORITY, id: "temp", created_at: new Date(), updated_at: new Date(), team_id: "temp", user_id: "temp", user: undefined, _count: { tags: 0 }, tags: [] }; // 断言为原Job类型,通过类型检查 utils.jobs.getAll.setData(undefined, [...previousJobs, optimisticJob as Job]); } },
方法3:复用mutation的输入类型
如果你的mutation输入类型(比如CreateJobInput)已经包含了用户提交的所有字段,可以基于它扩展乐观更新类型,避免重复定义:
import type { CreateJobInput, Job } from '@/server/trpc/routers/jobs'; type OptimisticJob = CreateJobInput & { id: string; status: JobStatus; priority: JobPriority; created_at: Date; updated_at: Date; team_id: string; user_id: string; // 补全关联属性的最小占位 user: Partial<Job['user']>; _count: { tags: 0, candidates: 0 }; tags: []; }; onMutate: (data: CreateJobInput) => { const previousJobs = utils.jobs.getAll.getData(); if (previousJobs) { const optimisticJob: OptimisticJob = { ...data, status: JobStatus.OPEN, priority: JobPriority.NO_PRIORITY, id: "temp", created_at: new Date(), updated_at: new Date(), team_id: "temp", user_id: "temp", user: {}, _count: { tags: 0, candidates: 0 }, tags: [] }; utils.jobs.getAll.setData(undefined, [...previousJobs, optimisticJob as Job]); } },
额外优化:添加错误回滚
别忘了在乐观更新失败时回滚数据,保证UI一致性:
onMutate: async (data) => { // 取消当前正在进行的查询,避免竞态问题 await utils.jobs.getAll.cancel(); const previousJobs = utils.jobs.getAll.getData(); if (previousJobs) { const optimisticJob: Job = { ...data, status: JobStatus.OPEN, priority: JobPriority.NO_PRIORITY, id: "temp", created_at: new Date(), updated_at: new Date(), team_id: "temp", user_id: "temp", user: {} as Job['user'], _count: { tags: 0, candidates: 0 }, tags: [] }; utils.jobs.getAll.setData(undefined, [...previousJobs, optimisticJob]); } // 返回上下文,用于onError时回滚 return { previousJobs }; }, onError: (err, variables, context) => { // 如果有上下文数据,恢复之前的查询结果 if (context?.previousJobs) { utils.jobs.getAll.setData(undefined, context.previousJobs); } },
内容的提问来源于stack exchange,提问作者Jesse Winton
相关产品推荐
相关产品推荐

