Migrar de v9 a v10
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.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,});
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.
- 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.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;
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.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. Crea un nuevo appRouter
-
Renombra tu antiguo
appRoutercomolegacyRouter -
Crea un nuevo router de aplicación:
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;
¡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.
Links personalizados
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// 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,},});
Cambios en las claves de consulta
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:
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 } })),}),});
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.
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();
Opciones específicas de HTTP movidas de TRPCClient a los links
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:
tsximport { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
tsximport { 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.