跳至主内容
版本:10.x

从 v9 迁移至 v10

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

欢迎使用 tRPC v10!我们很高兴为您带来这个全新主版本,继续推进端到端类型安全的完美旅程,同时提供卓越的开发体验。

在 v10 版本中,我们实现了性能优化,带来了开发体验的提升,并为未来构建新功能创造了空间。

tRPC v10 为 v9 用户提供了兼容层。.interop() 方法允许您逐步迁移到 v10,让您在享受 v10 新功能的同时继续开发项目。

变更摘要

Initializing your server
/src/server/trpc.ts
ts
/**
* 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.ts
ts
/**
* 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
// v9
client.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
// v9
client.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
// v9
const 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}!`;
},
});
// v10
const 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
// v9
const 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}!`;
},
});
// v10
const 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.ts
ts
import { 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.ts
ts
import { 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.ts
diff
const appRouter = trpc
.router<Context>()
/* ... */
+ .interop();
export type AppRouter = typeof appRouter;
src/server/routers/_app.ts
diff
const appRouter = trpc
.router<Context>()
/* ... */
+ .interop();
export type AppRouter = typeof appRouter;
信息

部分功能不被 .interop() 支持。我们预期绝大多数用户能在几分钟内通过 .interop() 完成服务端迁移。若发现 .interop() 无法正常工作,请查看此处

2. 创建 t 对象

现在初始化 v10 router,以便为后续新路由启用 v10 功能。

src/server/trpc.ts
ts
import { 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.ts
ts
import { 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

  1. 将旧的 appRouter 重命名为 legacyRouter

  2. 创建新的 app router:

ts
import { 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 router
export const appRouter = mergeRouters(legacyRouter, mainRouter);
export type AppRouter = typeof appRouter;
ts
import { 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 router
export 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

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 主版本升级

我们将 peerDependenciesreact-query@^3 升级至 @tanstack/react-query@^4。由于客户端钩子仅是 react-query 的轻量封装,建议查阅其迁移指南了解 React 钩子的新实现。

钩子中的 tRPC 专属选项移至 trpc 命名空间

为避免与 react-query 内置属性冲突,所有 tRPC 选项已移至 trpc 属性下。该命名空间明确了 tRPC 专属选项,确保未来不会与 react-query 产生冲突。

tsx
// Before
useQuery(['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
// Before
useQuery(['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和输入参数整合到具有命名属性的对象中。

给定以下简单路由:

tsx
export const appRouter = router({
user: router({
byId: publicProcedure
.input(z.object({ id: z.number() }))
.query((opts) => ({ user: { id: opts.input.id } })),
}),
});
tsx
export 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()

tsx
const ac = new AbortController();
const helloQuery = client.greeting.query('KATT', { signal: ac.signal });
// Aborting
ac.abort();
tsx
const ac = new AbortController();
const helloQuery = client.greeting.query('KATT', { signal: ac.signal });
// Aborting
ac.abort();

HTTP 专属选项从 TRPCClient 移至链接层

此前 HTTP 选项(如 headers)直接配置在 createTRPCClient() 上。鉴于 tRPC 本身不限于 HTTP 协议,我们已将这类配置从 TRPCClient 移至 httpLinkhttpBatchLink

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 函数不再允许返回 nullundefined。若未使用自定义上下文,需返回空对象:

diff
- createContext: () => null,
+ createContext: () => ({}),
diff
- createContext: () => null,
+ createContext: () => ({}),

queryClient 不再通过 tRPC 上下文暴露

tRPC 不再通过 trpc.useContext() 暴露 queryClient 实例。如需使用 queryClient 的一些方法,请检查 trpc.useContext() 是否封装了对应功能(详见此处)。若所需方法尚未封装,可直接从 @tanstack/react-query 导入 queryClient 使用:

tsx
import { useQueryClient } from '@tanstack/react-query';
const MyComponent = () => {
const queryClient = useQueryClient();
// ...
};
tsx
import { useQueryClient } from '@tanstack/react-query';
const MyComponent = () => {
const queryClient = useQueryClient();
// ...
};

迁移自定义错误格式化器

需将 formatError() 中的内容迁移至根 t 路由。具体操作请参考错误格式化文档