从 v9 迁移至 v10
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
欢迎使用 tRPC v10!我们很高兴为您带来这个全新主版本,继续推进端到端类型安全的完美旅程,同时提供卓越的开发体验。
在 v10 版本中,我们实现了性能优化,带来了开发体验的提升,并为未来构建新功能创造了空间。
tRPC v10 为 v9 用户提供了兼容层。.interop() 方法允许您逐步迁移到 v10,让您在享受 v10 新功能的同时继续开发项目。
变更摘要
Initializing your server
/src/server/trpc.tsts/*** This is your entry point to setup the root configuration for tRPC on the server.* - `initTRPC` should only be used once per app.* - We export only the functionality that we use so we can enforce which base procedures should be used** Learn how to create protected base procedures and other things below:* @see https://trpc.io/docs/v10/router* @see https://trpc.io/docs/v10/procedures*/import { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({/*** @see https://trpc.io/docs/v10/data-transformers*/transformer: superjson,/*** @see https://trpc.io/docs/v10/error-formatting*/errorFormatter(opts) {return opts.shape;},});/*** Create a router* @see https://trpc.io/docs/v10/router*/export const router = t.router;/*** Create an unprotected procedure* @see https://trpc.io/docs/v10/procedures**/export const publicProcedure = t.procedure;/*** @see https://trpc.io/docs/v10/merging-routers*/export const mergeRouters = t.mergeRouters;
/src/server/trpc.tsts/*** This is your entry point to setup the root configuration for tRPC on the server.* - `initTRPC` should only be used once per app.* - We export only the functionality that we use so we can enforce which base procedures should be used** Learn how to create protected base procedures and other things below:* @see https://trpc.io/docs/v10/router* @see https://trpc.io/docs/v10/procedures*/import { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({/*** @see https://trpc.io/docs/v10/data-transformers*/transformer: superjson,/*** @see https://trpc.io/docs/v10/error-formatting*/errorFormatter(opts) {return opts.shape;},});/*** Create a router* @see https://trpc.io/docs/v10/router*/export const router = t.router;/*** Create an unprotected procedure* @see https://trpc.io/docs/v10/procedures**/export const publicProcedure = t.procedure;/*** @see https://trpc.io/docs/v10/merging-routers*/export const mergeRouters = t.mergeRouters;
Defining routers & procedures
ts// v9:const appRouter = trpc.router().query('greeting', {input: z.string(),resolve(opts) {return `hello ${opts.input}!`;},});// v10:const appRouter = router({greeting: publicProcedure.input(z.string()).query((opts) => `hello ${opts.input}!`),});
ts// v9:const appRouter = trpc.router().query('greeting', {input: z.string(),resolve(opts) {return `hello ${opts.input}!`;},});// v10:const appRouter = router({greeting: publicProcedure.input(z.string()).query((opts) => `hello ${opts.input}!`),});
Calling procedures
ts// v9client.query('greeting', 'KATT');trpc.useQuery(['greeting', 'KATT']);// v10// You can now CMD+click `greeting` to jump straight to your server code.client.greeting.query('KATT');trpc.greeting.useQuery('KATT');
ts// v9client.query('greeting', 'KATT');trpc.useQuery(['greeting', 'KATT']);// v10// You can now CMD+click `greeting` to jump straight to your server code.client.greeting.query('KATT');trpc.greeting.useQuery('KATT');
Inferring types
v9
ts// Building multiple complex helper types yourself. Yuck!export type TQuery = keyof AppRouter['_def']['queries'];export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<AppRouter['_def']['queries'][TRouteKey]>;type GreetingInput = InferQueryInput<'greeting'>;
ts// Building multiple complex helper types yourself. Yuck!export type TQuery = keyof AppRouter['_def']['queries'];export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<AppRouter['_def']['queries'][TRouteKey]>;type GreetingInput = InferQueryInput<'greeting'>;
v10
Inference helpers
ts// Inference helpers are now shipped out of the box.import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';import type { AppRouter } from './server';type RouterInput = inferRouterInputs<AppRouter>;type RouterOutput = inferRouterOutputs<AppRouter>;type PostCreateInput = RouterInput['post']['create'];// ^?type PostCreateOutput = RouterOutput['post']['create'];// ^?
ts// Inference helpers are now shipped out of the box.import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';import type { AppRouter } from './server';type RouterInput = inferRouterInputs<AppRouter>;type RouterOutput = inferRouterOutputs<AppRouter>;type PostCreateInput = RouterInput['post']['create'];// ^?type PostCreateOutput = RouterOutput['post']['create'];// ^?
See Inferring types for more.
Middlewares
Middlewares are now reusable and can be chained, see the middleware docs for more.
ts// v9const appRouter = trpc.router().middleware((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {...ctx,user: ctx.user,},});}).query('greeting', {resolve(opts) {return `hello ${opts.ctx.user.name}!`;},});// v10const protectedProcedure = t.procedure.use((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {// Old context will automatically be spread.// Only modify what's changed.user: ctx.user,},});});const appRouter = t.router({greeting: protectedProcedure.query((opts) => {return `Hello ${opts.ctx.user.name}!`}),});
ts// v9const appRouter = trpc.router().middleware((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {...ctx,user: ctx.user,},});}).query('greeting', {resolve(opts) {return `hello ${opts.ctx.user.name}!`;},});// v10const protectedProcedure = t.procedure.use((opts) => {const { ctx } = opts;if (!ctx.user) {throw new TRPCError({ code: 'UNAUTHORIZED' });}return opts.next({ctx: {// Old context will automatically be spread.// Only modify what's changed.user: ctx.user,},});});const appRouter = t.router({greeting: protectedProcedure.query((opts) => {return `Hello ${opts.ctx.user.name}!`}),});
Full example with data transformer, OpenAPI metadata, and error formatter
/src/server/trpc.tstsimport { initTRPC } from '@trpc/server';import superjson from 'superjson';// Context is usually inferred,// but we will need it here for this example.interface Context {user?: {id: string;name: string;};}interface Meta {openapi: {enabled: boolean;method: string;path: string;};}export const t = initTRPC.context<Context>().meta<Meta>().create({errorFormatter({ shape, error }) {return {...shape,data: {...shape.data,zodError:error.code === 'BAD_REQUEST' && error.cause instanceof ZodError? error.cause.flatten(): null,},};},transformer: superjson,});
/src/server/trpc.tstsimport { initTRPC } from '@trpc/server';import superjson from 'superjson';// Context is usually inferred,// but we will need it here for this example.interface Context {user?: {id: string;name: string;};}interface Meta {openapi: {enabled: boolean;method: string;path: string;};}export const t = initTRPC.context<Context>().meta<Meta>().create({errorFormatter({ shape, error }) {return {...shape,data: {...shape.data,zodError:error.code === 'BAD_REQUEST' && error.cause instanceof ZodError? error.cause.flatten(): null,},};},transformer: superjson,});
从 v9 迁移
我们推荐两种策略来启动(并完成!)您的代码库升级。
使用 codemod
@sachinraja 为此次重大升级开发了出色的 codemod 工具。运行该脚本可在瞬间完成 95% 的迁移工作。
- 即使使用 codemod,仍需执行下方的步骤 1-3,确保其适用于您的项目后再进行完整迁移
- 请注意该 codemod 并非完美,但能为您承担大量繁重工作
使用 .interop()
一次性重写所有 v9 路由可能对您和团队负担过重。建议保留现有 v9 过程,利用 v10 的 interop() 方法逐步迁移。
1. 在 v9 router 上启用 interop()
只需添加 10 个字符即可将 v9 router 转换为 v10 router。在 v9 router 末尾加上 .interop()... 服务端代码的迁移就完成了!
src/server/routers/_app.tsdiffconst appRouter = trpc.router<Context>()/* ... */+ .interop();export type AppRouter = typeof appRouter;
src/server/routers/_app.tsdiffconst appRouter = trpc.router<Context>()/* ... */+ .interop();export type AppRouter = typeof appRouter;
部分功能不被 .interop() 支持。我们预期绝大多数用户能在几分钟内通过 .interop() 完成服务端迁移。若发现 .interop() 无法正常工作,请查看此处。
2. 创建 t 对象
现在初始化 v10 router,以便为后续新路由启用 v10 功能。
src/server/trpc.tstsimport { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({// Optional:transformer: superjson,// Optional:errorFormatter(opts) {const { shape } = opts;return {...shape,data: {...shape.data,},};},});/*** We recommend only exporting the functionality that we* use so we can enforce which base procedures should be used**/export const router = t.router;export const mergeRouters = t.mergeRouters;export const publicProcedure = t.procedure;
src/server/trpc.tstsimport { initTRPC } from '@trpc/server';import superjson from 'superjson';import { Context } from './context';const t = initTRPC.context<Context>().create({// Optional:transformer: superjson,// Optional:errorFormatter(opts) {const { shape } = opts;return {...shape,data: {...shape.data,},};},});/*** We recommend only exporting the functionality that we* use so we can enforce which base procedures should be used**/export const router = t.router;export const mergeRouters = t.mergeRouters;export const publicProcedure = t.procedure;
3. 创建新的 appRouter
-
将旧的
appRouter重命名为legacyRouter -
创建新的 app router:
tsimport { mergeRouters, publicProcedure, router } from './trpc';// Renamed from `appRouter`const legacyRouter = trpc.router()/* ... */.interop();const mainRouter = router({greeting: publicProcedure.query(() => 'hello from tRPC v10!'),});// Merge v9 router with v10 routerexport const appRouter = mergeRouters(legacyRouter, mainRouter);export type AppRouter = typeof appRouter;
tsimport { mergeRouters, publicProcedure, router } from './trpc';// Renamed from `appRouter`const legacyRouter = trpc.router()/* ... */.interop();const mainRouter = router({greeting: publicProcedure.query(() => 'hello from tRPC v10!'),});// Merge v9 router with v10 routerexport const appRouter = mergeRouters(legacyRouter, mainRouter);export type AppRouter = typeof appRouter;
注意避免使用会产生相同调用者名称的过程!如果 legacy router 中的路径与 new router 中的路径匹配,将会引发问题。
4. 在客户端使用
两组过程现在都将作为 v10 调用者供客户端使用。您需要访问客户端代码将调用语法更新为 v10 格式。
ts// Vanilla JS v10 client caller:client.proxy.greeting.query();// React v10 client caller:trpc.proxy.greeting.useQuery();
ts// Vanilla JS v10 client caller:client.proxy.greeting.query();// React v10 client caller:trpc.proxy.greeting.useQuery();
interop 的限制
订阅功能
我们已更改订阅 API,现在订阅需要返回 observable 实例。详见订阅文档。
🚧 欢迎贡献改进此部分内容
自定义 HTTP 选项
请参阅HTTP 专属选项已从 TRPCClient 移至 links。
自定义 Links
v10 中 Links 架构已彻底重构,因此 v9 的自定义 links 在 v10 或 interop 模式下均无法使用。如需创建 v10 自定义 link,请查看Links 文档。
客户端包变更
v10 也为应用客户端带来变更,完成以下关键修改后即可获得多项体验优化:
- 直接从客户端跳转至服务端定义
- 在客户端直接重命名路由或过程
@trpc/react-query
@trpc/react 重命名为 @trpc/react-query
@trpc/react 包已更名为 @trpc/react-query,这既体现其作为 react-query 轻量封装层的定位,也为将来可能出现的场景(如 React Server Components (RSCs) 或其他数据获取库适配器)留出空间。使用 @trpc/react 的用户需卸载旧包并安装 @trpc/react-query,同时更新导入路径:
diff- import { createReactQueryHooks } from '@trpc/react';+ import { createReactQueryHooks } from '@trpc/react-query';
diff- import { createReactQueryHooks } from '@trpc/react';+ import { createReactQueryHooks } from '@trpc/react-query';
react-query 主版本升级
我们将 peerDependencies 从 react-query@^3 升级至 @tanstack/react-query@^4。由于客户端钩子仅是 react-query 的轻量封装,建议查阅其迁移指南了解 React 钩子的新实现。
钩子中的 tRPC 专属选项移至 trpc 命名空间
为避免与 react-query 内置属性冲突,所有 tRPC 选项已移至 trpc 属性下。该命名空间明确了 tRPC 专属选项,确保未来不会与 react-query 产生冲突。
tsx// BeforeuseQuery(['post.byId', '1'], {context: {batching: false,},});// After:useQuery(['post.byId', '1'], {trpc: {context: {batching: false,},},});// or:trpc.post.byId.useQuery('1', {trpc: {batching: false,},});
tsx// BeforeuseQuery(['post.byId', '1'], {context: {batching: false,},});// After:useQuery(['post.byId', '1'], {trpc: {context: {batching: false,},},});// or:trpc.post.byId.useQuery('1', {trpc: {batching: false,},});
查询键变更
若应用中仅使用 tRPC 提供的 API,迁移过程将无任何障碍 👍 但若曾直接使用 tanstack query client 进行跨查询操作(例如使用 queryClient.setQueriesData 更新多个 tRPC 生成查询的数据),则需特别注意!
为实现跨整个路由失效等高级功能,我们需要修改 tanstack 底层查询键的实现机制。
我们已更改查询键的生成方式:从使用点号(.)连接的过程路径字符串,改为使用子元素数组结构。同时新增了缓存中query(普通查询)与infinite(无限查询)的区分机制,并将查询type和输入参数整合到具有命名属性的对象中。
给定以下简单路由:
tsxexport const appRouter = router({user: router({byId: publicProcedure.input(z.object({ id: z.number() })).query((opts) => ({ user: { id: opts.input.id } })),}),});
tsxexport const appRouter = router({user: router({byId: publicProcedure.input(z.object({ id: z.number() })).query((opts) => ({ user: { id: opts.input.id } })),}),});
trpc.user.byId.useQuery({ id: 10 }) 使用的查询键将变为:
-
v9中的键:
["user.byId", { id: 10 }] -
v10中的键:
[["user", "byId"],{ input: { id:10 }, type: 'query' }]
大多数开发者不会察觉此变更,但少数直接使用 tanstack queryClient 操作 tRPC 生成查询的用户,需调整其过滤逻辑使用的键!
@trpc/client
中止过程
v9 使用 .cancel() 方法中止过程。
v10 改为采用 AbortController Web API 以更好遵循 Web 标准。你需要为查询提供 AbortSignal,并在其父级 AbortController 上调用 .abort() 来替代 .cancel()。
tsxconst ac = new AbortController();const helloQuery = client.greeting.query('KATT', { signal: ac.signal });// Abortingac.abort();
tsxconst ac = new AbortController();const helloQuery = client.greeting.query('KATT', { signal: ac.signal });// Abortingac.abort();
HTTP 专属选项从 TRPCClient 移至链接层
此前 HTTP 选项(如 headers)直接配置在 createTRPCClient() 上。鉴于 tRPC 本身不限于 HTTP 协议,我们已将这类配置从 TRPCClient 移至 httpLink 和 httpBatchLink。
ts// Before:import { createTRPCClient } from '@trpc/client';const client = createTRPCClient({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},});// After:import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';const client = createTRPCProxyClient({links: [httpBatchLink({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},})]});
ts// Before:import { createTRPCClient } from '@trpc/client';const client = createTRPCClient({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},});// After:import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';const client = createTRPCProxyClient({links: [httpBatchLink({url: '...',fetch: myFetchPonyfill,AbortController: myAbortControllerPonyfill,headers() {return {'x-foo': 'bar',};},})]});
此变更同样体现在 @trpc/server 包中:原先从主入口导出的 http 相关功能,现已移至独立的 @trpc/server/http 入口点。
其他变更
移除 teardown 选项
teardown 选项已被移除且不再可用。
createContext 返回类型
createContext 函数不再允许返回 null 或 undefined。若未使用自定义上下文,需返回空对象:
diff- createContext: () => null,+ createContext: () => ({}),
diff- createContext: () => null,+ createContext: () => ({}),
queryClient 不再通过 tRPC 上下文暴露
tRPC 不再通过 trpc.useContext() 暴露 queryClient 实例。如需使用 queryClient 的一些方法,请检查 trpc.useContext() 是否封装了对应功能(详见此处)。若所需方法尚未封装,可直接从 @tanstack/react-query 导入 queryClient 使用:
tsximport { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
tsximport { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
迁移自定义错误格式化器
需将 formatError() 中的内容迁移至根 t 路由。具体操作请参考错误格式化文档。