You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 19:10:32