Saltar al contenido principal
Versión: 10.x

Migrar de v9 a v10

Traducción Beta No Oficial

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

¡Bienvenido a tRPC v10! Estamos emocionados de presentarte esta nueva versión principal para continuar el camino hacia una seguridad de tipos de extremo a extremo perfecta con una excelente experiencia de desarrollo.

Bajo el capó de la versión 10, estamos desbloqueando mejoras de rendimiento, incorporando optimizaciones para facilitar tu trabajo diario y creando espacio para que podamos desarrollar nuevas funcionalidades en el futuro.

tRPC v10 incluye una capa de compatibilidad para usuarios que vienen de v9. .interop() te permite adoptar v10 gradualmente, permitiéndote continuar construyendo el resto de tu proyecto mientras disfrutas de las nuevas características de v10.

Resumen de cambios

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,
});

Migrando desde v9

Te recomendamos dos estrategias para comenzar (¡y completar!) la actualización de tu código hoy mismo.

Usando un codemod

@sachinraja ha creado un codemod excelente para esta actualización mayor. Ejecuta el script y tendrás el 95% del trabajo resuelto en cuestión de minutos.

información
  • Si usas el codemod, aún debes realizar los pasos 1-3 a continuación para verificar que funciona correctamente antes de migrar completamente.
  • Ten en cuenta que este codemod no es perfecto, pero hará gran parte del trabajo pesado por ti.

Usando .interop()

Reescribir todas tus rutas v9 existentes hoy podría ser demasiado complejo para ti y tu equipo. En su lugar, mantengamos esos procedimientos v9 y adoptemos v10 gradualmente aprovechando el método interop() de v10.

1. Habilita interop() en tu router v9

Convertir tu router v9 en un router v10 toma solo 10 caracteres. Agrega .interop() al final de tu router v9... ¡y listo con tu código del servidor!

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;
información

Existen algunas características no soportadas por .interop(). Esperamos que casi todos nuestros usuarios puedan usar .interop() para migrar su código del servidor en solo minutos. Si descubres que .interop() no funciona correctamente, consulta aquí.

2. Crea el objeto t

Ahora, inicialicemos un router v10 para comenzar a usar v10 en cualquier nueva ruta que escribamos.

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. Crea un nuevo appRouter

  1. Renombra tu antiguo appRouter como legacyRouter

  2. Crea un nuevo router de aplicación:

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;
consejo

¡Ten cuidado al usar procedimientos que terminen teniendo el mismo nombre de invocación! Encontrarás problemas si una ruta en tu router heredado coincide con una ruta en tu nuevo router.

4. Úsalo en tu cliente

Ambos conjuntos de procedimientos ahora estarán disponibles para tu cliente como invocadores v10. Ahora necesitarás visitar tu código cliente para actualizar tus invocadores a la sintaxis 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();

Limitaciones de interop

Suscripciones

Hemos cambiado la API de Suscripciones donde las suscripciones deben retornar una instancia observable. Consulta la documentación de suscripciones.

🚧 Siéntete libre de contribuir para mejorar esta sección

Opciones HTTP personalizadas

Consulta las opciones específicas de HTTP movidas de TRPCClient a los links.

En v10, la arquitectura de Links ha sido completamente renovada. Por lo tanto, los links personalizados creados para v9 no funcionarán en v10 ni durante interop. Si quieres más información sobre cómo crear un link personalizado para v10, revisa la documentación de Links.

Cambios en el paquete cliente

v10 también trae cambios al lado cliente de tu aplicación. Después de hacer algunos cambios clave, desbloquearás mejoras importantes en la experiencia de desarrollo:

  • Saltar a definiciones del servidor directamente desde tu cliente
  • Renombrar routers o procedimientos directamente desde el cliente

@trpc/react-query

Renombrado de @trpc/react a @trpc/react-query

El paquete @trpc/react ha sido renombrado a @trpc/react-query. Esto refleja que es una capa delgada sobre react-query, y permite situaciones donde trpc puede usarse en React sin el paquete @trpc/react-query, como con los próximos React Server Components (RSCs) o con otros adaptadores de bibliotecas de obtención de datos. Si estás usando @trpc/react, necesitarás eliminarlo e instalar @trpc/react-query en su lugar, además de actualizar tus imports:

diff
- import { createReactQueryHooks } from '@trpc/react';
+ import { createReactQueryHooks } from '@trpc/react-query';
diff
- import { createReactQueryHooks } from '@trpc/react';
+ import { createReactQueryHooks } from '@trpc/react-query';

Actualización de versión mayor de react-query

Hemos actualizado los peerDependencies de react-query@^3 a @tanstack/react-query@^4. Como nuestros hooks cliente son solo una capa delgada sobre react-query, te recomendamos visitar su guía de migración para más detalles sobre tu nueva implementación de hooks React.

Opciones específicas de tRPC en hooks movidas a trpc

Para evitar colisiones y confusiones con propiedades integradas de react-query, hemos movido todas las opciones de tRPC a una propiedad llamada trpc. Este espacio de nombres brinda claridad a las opciones específicas de tRPC y asegura que no colisionaremos con react-query en el futuro.

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,
},
});

Cambios en las claves de consulta

nota

Si solo usas las APIs proporcionadas por tRPC en tu app, no tendrás problemas al migrar 👍 Sin embargo, si has estado usando el cliente tanstack query directamente para hacer cosas como actualizar datos de consulta para múltiples consultas generadas por tRPC usando queryClient.setQueriesData, ¡deberás tomar nota!

Para permitirnos hacer espacio para características más avanzadas como invalidación a través de routers completos, necesitábamos cambiar cómo usamos las claves de consulta de tanstack bajo el capó.

Hemos modificado las claves de consulta que utilizamos: en lugar de una cadena unida por . para la ruta del procedimiento, ahora usamos un subarray de elementos. También hemos añadido una distinción entre consultas query e infinite cuando se colocan en la caché. Además, hemos trasladado tanto este type de consulta como el input a un objeto con propiedades nombradas.

Dado el siguiente enrutador simple:

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 } })),
}),
});

La clave de consulta utilizada para trpc.user.byId.useQuery({ id: 10 }) cambiaría:

  • Clave en V9: ["user.byId", { id: 10 }]

  • Clave en v10: [["user", "byId"],{ input: { id:10 }, type: 'query' }]

La mayoría de los desarrolladores ni siquiera notarán este cambio, pero la pequeña minoría que usa queryClient de TanStack directamente para manipular consultas generadas por tRPC deberá ajustar las claves que utilizan para filtrar.

@trpc/client

Cancelación de procedimientos

En v9, se usaba el método .cancel() para abortar procedimientos.

En v10, nos hemos alineado con la API Web AbortController para seguir mejor los estándares web. En lugar de llamar a .cancel(), deberás proporcionar a la consulta una señal AbortSignal y llamar a .abort() en su AbortController padre.

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();

Anteriormente, las opciones HTTP (como headers) se colocaban directamente en tu createTRPCClient(). Sin embargo, como tRPC técnicamente no está ligado a HTTP, las hemos trasladado desde TRPCClient a httpLink y 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',
};
},
})
]
});

Este cambio también se refleja en el paquete @trpc/server, donde las exportaciones relacionadas con http anteriormente venían del punto de entrada principal, pero ahora se han movido a su propio punto de entrada @trpc/server/http.

Extras

Eliminación de la opción teardown

La opción teardown ha sido eliminada y ya no está disponible.

Tipo de retorno de createContext

La función createContext ya no puede devolver null o undefined. Si no estabas usando un contexto personalizado, deberás devolver un objeto vacío:

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

queryClient ya no se expone a través del contexto de tRPC

tRPC ya no expone la instancia de queryClient mediante trpc.useContext(). Si necesitas usar algunos métodos de queryClient, verifica si trpc.useContext() los incluye aquí. Si tRPC aún no incluye el método respectivo, puedes importar queryClient desde @tanstack/react-query y usarlo de esa manera:

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

Migrar formateadores de errores personalizados

Deberás mover el contenido de tu formatError() a tu enrutador raíz t. Consulta la documentación de Formateo de Errores para más detalles.