Migrer de la v9 vers la v10
Cette page a été traduite par PageTurner AI (bêta). Non approuvée officiellement par le projet. Vous avez trouvé une erreur ? Signaler un problème →
Bienvenue dans tRPC v10 ! Nous sommes ravis de vous présenter cette nouvelle version majeure pour poursuivre notre quête d'une sécurité de type bout-en-bout parfaite avec une excellente expérience développeur.
Sous le capot de la version 10, nous débloquons des améliorations de performance, apportons des optimisations de confort d'utilisation et créons l'espace nécessaire pour développer de nouvelles fonctionnalités futures.
tRPC v10 inclut une couche de compatibilité pour les utilisateurs venant de la v9. La méthode .interop() vous permet d'adopter progressivement la v10 tout en continuant à développer votre projet et à bénéficier des nouvelles fonctionnalités.
Résumé des changements
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,});
Migration depuis la v9
Nous recommandons deux stratégies pour commencer (et terminer !) la mise à jour de votre codebase dès aujourd'hui.
Utilisation d'un codemod
@sachinraja a créé un excellent codemod pour cette mise à jour majeure. Exécutez le script pour réaliser 95% du travail en quelques instants.
- Si vous utilisez le codemod, suivez tout de même les étapes 1 à 3 ci-dessous pour vérifier son bon fonctionnement avant la migration complète.
- Notez que ce codemod n'est pas parfait mais effectuera l'essentiel du travail pour vous.
Utilisation de .interop()
Réécrire toutes vos routes v9 existantes pourrait représenter une charge trop importante pour votre équipe. Conservez plutôt vos procédures v9 et adoptez progressivement la v10 en utilisant la méthode interop().
1. Activer interop() sur votre routeur v9
Transformer votre router v9 en router v10 ne nécessite que 10 caractères. Ajoutez .interop() à la fin de votre router v9... et votre code serveur est prêt !
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;
Certaines fonctionnalités ne sont pas supportées par .interop(). Nous estimons que la quasi-totalité des utilisateurs pourront migrer leur code serveur en quelques minutes grâce à .interop(). Si vous constatez que .interop() ne fonctionne pas correctement, consultez la section limitations.
2. Créer l'objet t
Initialisons maintenant un router v10 pour pouvoir l'utiliser sur toutes les nouvelles routes.
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. Créer un nouveau appRouter
-
Renommez votre ancien
appRouterenlegacyRouter -
Créez un nouveau router d'application :
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;
Attention à l'utilisation de procédures qui finiront par avoir le même nom d'appelant ! Vous rencontrerez des problèmes si un chemin dans votre routeur hérité correspond à un chemin dans votre nouveau routeur.
4. Utilisez-le dans votre client
Les deux ensembles de procédures seront désormais disponibles pour votre client en tant qu'appelants v10. Vous devrez maintenant consulter le code de votre client pour mettre à jour vos appelants avec la syntaxe 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();
Limitations de l'interop
Abonnements
Nous avons modifié l'API des abonnements, qui doivent désormais retourner une instance observable. Consultez la documentation sur les abonnements.
🚧 N'hésitez pas à contribuer pour améliorer cette section
Options HTTP personnalisées
Voir les options spécifiques à HTTP déplacées de TRPCClient vers les liens.
Liens personnalisés
Dans v10, l'architecture des liens a été complètement revue. Par conséquent, les liens personnalisés créés pour v9 ne fonctionneront pas pour v10 ou pendant l'utilisation de l'interop. Si vous souhaitez plus d'informations sur la création d'un lien personnalisé pour v10, consultez la documentation sur les liens.
Modifications du côté client
v10 apporte également des modifications côté client de votre application. Après quelques changements clés, vous débloquerez quelques améliorations de qualité de vie :
- Accédez directement aux définitions du serveur depuis votre client
- Renommez des routeurs ou des procédures directement depuis le client
@trpc/react-query
Renommage de @trpc/react en @trpc/react-query
Le package @trpc/react a été renommé en @trpc/react-query. Cela reflète le fait qu'il s'agit d'une fine couche autour de react-query, et permet également des situations où trpc pourrait être utilisé dans React sans le package @trpc/react-query, comme avec les futurs Composants Serveur React (RSC) ou avec d'autres adaptateurs de bibliothèques de récupération de données. Si vous utilisez @trpc/react, vous devrez le supprimer et installer @trpc/react-query à la place, et mettre à jour vos 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';
Mise à jour majeure de react-query
Nous avons mis à jour les peerDependencies de react-query@^3 vers @tanstack/react-query@^4. Comme nos hooks client ne sont qu'une fine couche autour de react-query, nous vous encourageons à consulter leur guide de migration pour plus de détails sur votre nouvelle implémentation des hooks React.
Options spécifiques à tRPC dans les hooks déplacées vers trpc
Pour éviter les collisions et la confusion avec les propriétés intégrées de react-query, nous avons déplacé toutes les options tRPC vers une propriété appelée trpc. Cet espace de noms apporte de la clarté aux options spécifiques à tRPC et garantit que nous n'entrerons pas en collision avec react-query à l'avenir.
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,},});
Changements des clés de requête
Si vous n'utilisez que les API fournies par tRPC dans votre application, vous n'aurez aucun problème lors de la migration 👍 Cependant, si vous avez utilisé directement le client tanstack query pour faire des choses comme mettre à jour les données de requête pour plusieurs requêtes générées par tRPC en utilisant queryClient.setQueriesData, vous devriez en prendre note !
Pour nous permettre de faire de la place à des fonctionnalités plus avancées comme l'invalidation sur des routeurs entiers, nous avons dû modifier la façon dont nous utilisons les clés de requête tanstack sous le capot.
Nous avons modifié les clés de requête : au lieu d'utiliser une chaîne de caractères jointe par des . pour le chemin des procédures, nous utilisons désormais un sous-tableau d'éléments. Nous avons également introduit une distinction entre les requêtes query et infinite lors de leur placement dans le cache. De plus, nous avons regroupé le type de requête et l'input dans un objet avec des propriétés nommées.
Prenons le routeur simple ci-dessous :
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 clé de requête utilisée pour trpc.user.byId.useQuery({ id: 10 }) changerait ainsi :
-
Clé en V9 :
["user.byId", { id: 10 }] -
Clé en v10 :
[["user", "byId"],{ input: { id:10 }, type: 'query' }]
La majorité des développeurs ne remarqueront même pas ce changement, mais la petite minorité qui manipule directement les requêtes tRPC via queryClient de TanStack devra adapter les clés utilisées pour le filtrage !
@trpc/client
Interruption des procédures
Dans la v9, la méthode .cancel() était utilisée pour interrompre les procédures.
Dans la v10, nous adoptons l'API Web AbortController pour mieux respecter les standards du web. Au lieu d'appeler .cancel(), vous fournirez un AbortSignal à la requête et appellerez .abort() sur son AbortController parent.
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();
Options HTTP déplacées du TRPCClient vers les liens
Auparavant, les options HTTP (comme les headers) étaient placées directement dans votre createTRPCClient(). Cependant, comme tRPC n'est techniquement pas lié à HTTP, nous les avons déplacées du TRPCClient vers httpLink et 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',};},})]});
Ce changement se reflète également dans le package @trpc/server, où les exports liés à http étaient auparavant dans l'entrée principale mais ont été déplacés vers leur propre entrée @trpc/server/http.
Compléments
Suppression de l'option teardown
L'option teardown a été retirée et n'est plus disponible.
Type de retour de createContext
La fonction createContext ne peut plus retourner null ou undefined. Si vous n'utilisiez pas de contexte personnalisé, vous devrez retourner un objet vide :
diff- createContext: () => null,+ createContext: () => ({}),
diff- createContext: () => null,+ createContext: () => ({}),
queryClient n'est plus exposé via le contexte tRPC
tRPC n'expose plus l'instance queryClient via trpc.useContext(). Si vous avez besoin d'utiliser certaines méthodes de queryClient, vérifiez si trpc.useContext() les fournit ici. Si la méthode n'est pas encore encapsulée par tRPC, vous pouvez importer queryClient depuis @tanstack/react-query et l'utiliser directement :
tsximport { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
tsximport { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
Migrer les formateurs d'erreurs personnalisés
Vous devrez déplacer le contenu de votre formatError() dans votre routeur t racine. Consultez la documentation sur le formatage des erreurs pour plus de détails.