Hoppa till huvudinnehållet
Version: 10.x

Migrera från v9 till v10

Inofficiell Beta-översättning

Denna sida har översatts av PageTurner AI (beta). Inte officiellt godkänd av projektet. Hittade du ett fel? Rapportera problem →

Välkommen till tRPC v10! Vi är glada att presentera en ny major version som fortsätter resan mot perfekt typ-säkerhet från slut till slut med utmärkt utvecklarupplevelse (DX).

Under huven i version 10 har vi öppnat för prestandaförbättringar, implementerat livskvalitetsförbättringar och skapat utrymme för att bygga nya funktioner i framtiden.

tRPC v10 innehåller ett kompatibilitetslager för användare som kommer från v9. .interop() låter dig adoptera v10 stegvis så att du kan fortsätta bygga på resten av ditt projekt samtidigt som du nyttjar v10:s nya funktioner.

Sammanfattning av ändringar

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

Migrera från v9

Vi rekommenderar två strategier för att påbörja (och slutföra!) uppgraderingen av din kodbas redan idag.

Använda en codemod

@sachinraja har skapat en utmärkt codemod för denna major uppgradering. Kör skriptet för att få 95% av arbetet gjort åt dig på några ögonblick.

info
  • Om du använder codemod bör du fortfarande göra steg 1-3 nedan för att säkerställa att den fungerar för dig innan du gör en fullständig migrering.
  • Observera att denna codemod inte är perfekt men kommer att göra det mesta av det tunga lyftet åt dig.

Använda .interop()

Att skriva om alla dina befintliga v9-rutter idag kan bli för tungt för dig och ditt team. Behåll istället dina v9-procedurer och adoptera v10 stegvis genom att använda v10:s interop()-metod.

1. Aktivera interop() på din v9-router

Att omvandla din v9-router till en v10-router kräver bara 10 tecken. Lägg till .interop() i slutet av din v9-router... så är din serverkod klar!

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

Det finns några funktioner som inte stöds av .interop(). Vi förväntar oss att nästan alla användare ska kunna använda .interop() för att migrera sin serverkod på bara några minuter. Om du upptäcker att .interop() inte fungerar korrekt för dig, se till att kolla här.

2. Skapa t-objektet

Låt oss nu initiera en v10-router så att vi kan börja använda v10 för alla nya rutter vi ska skriva.

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. Skapa en ny appRouter

  1. Byt namn på din gamla appRouter till legacyRouter

  2. Skapa en ny 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;
tips

Var försiktig med att använda procedurer som kommer att få samma anroparnamn! Du kommer att stöta på problem om en sökväg i din legacy-router matchar en sökväg i din nya router.

tips

4. Använd den i din klient

Båda uppsättningarna procedurer kommer nu att vara tillgängliga för din klient som v10-anropare. Du måste nu besöka din klientkod för att uppdatera dina anrop till v10-syntaxen.

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

Begränsningar med interop

Prenumerationer

Vi har ändrat API:et för prenumerationer så att prenumerationer nu måste returnera en observable-instans. Se dokumentation för prenumerationer.

🚧 Du är välkommen att bidra till att förbättra detta avsnitt

Anpassade HTTP-alternativ

Se HTTP-specifika alternativ flyttade från TRPCClient till länkar.

Anpassade länkar

I v10 har länkarkitekturen genomgått en helomvändning. Därmed kommer anpassade länkar skapade för v9 inte fungera för v10 eller när du använder interop. Om du vill ha mer information om hur du skapar en anpassad länk för v10, kolla in länkdokumentationen.

Ändringar i klientpaket

v10 medför också ändringar på klientsidan i din applikation. Efter några nyckeländringar kommer du att få tillgång till flera förbättringar:

  • Hoppa direkt till serverdefinitioner från din klient
  • Byt namn på routrar eller procedurer direkt från klienten

@trpc/react-query

Omdöping av @trpc/react till @trpc/react-query

Paketet @trpc/react har bytt namn till @trpc/react-query. Detta för att spegla att det är ett tunt lager ovanpå react-query, samt för att tillåta situationer där tRPC kan användas i React utan @trpc/react-query-paketet, som med kommande React Server Components (RSCs) eller med andra adapterbibliotek för datahämtning. Om du använder @trpc/react måste du ta bort det och installera @trpc/react-query istället, samt uppdatera dina importer:

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

Huvudversionsuppgradering av react-query

Vi har uppgraderat peerDependencies från react-query@^3 till @tanstack/react-query@^4. Eftersom våra klienthooks bara är ett tunt lager ovanpå react-query uppmanar vi dig att besöka deras migrationsguide för mer information om din nya implementering av React-hooks.

tRPC-specifika alternativ för hooks flyttade till trpc

För att undvika kollisioner och förvirring med inbyggda react-query-egenskaper har vi flyttat alla tRPC-alternativ till en egenskap som heter trpc. Detta namnutrymme ger tydlighet kring alternativ som är specifika för tRPC och säkerställer att vi inte kommer att kollidera med react-query i framtiden.

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

Ändringar i frågenycklar

notering

Om du bara använder tRPC:s API:er i din app kommer du inte att stöta på några problem vid migrering 👍 Men om du har använt tanstack query-klienten direkt för att göra saker som att uppdatera frågedata för flera tRPC-genererade frågor med hjälp av queryClient.setQueriesData, bör du ta nota av följande!

För att göra plats för mer avancerade funktioner som invalidering över hela routrar behövde vi ändra hur vi använder tanstack-frågenycklar under huven.

Vi har ändrat våra frågenycklar från att använda en . separerad sträng för proceduresökvägen till en underarray av element. Vi har också infört en distinktion mellan query- och infinite-frågor när de placeras i cachen. Dessutom har vi flyttat både denna frågetyp (type) och input till ett objekt med namngivna egenskaper.

Med följande enkla router som exempel:

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

Frågenyckeln för trpc.user.byId.useQuery({ id: 10 }) skulle ändras:

  • Nyckel i v9: ["user.byId", { id: 10 }]

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

De flesta utvecklare kommer inte ens att märka denna förändring, men för den lilla minoritet som använder tanstack queryClient direkt för att manipulera tRPC-genererade frågor kommer de att behöva ändra vilken nyckel de filtrerar på!

@trpc/client

Avbryta procedurer

I v9 användes metoden .cancel() för att avbryta procedurer.

I v10 har vi övergått till AbortController Web API för bättre anpassning till webbstandarder. Istället för att anropa .cancel() ger du frågan en AbortSignal och anropar .abort() på dess överordnade AbortController.

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-specifika alternativ flyttade från TRPCClient till länkar

Tidigare placerades HTTP-alternativ (som headers) direkt i din createTRPCClient(). Eftersom tRPC tekniskt sett inte är bunden till HTTP har vi dock flyttat dessa från TRPCClient till httpLink och 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',
};
},
})
]
});

Denna förändring återspeglas också i @trpc/server-paketet, där http-relaterade exportörer tidigare fanns i huvudentryn men nu har flyttats till sin egen @trpc/server/http-entry.

Extraändringar

Borttagning av teardown-alternativet

Teardown-alternativet har tagits bort och är inte längre tillgängligt.

Returtyp för createContext

createContext-funktionen kan inte längre returnera null eller undefined. Om du inte använde en anpassad kontext måste du returnera ett tomt objekt:

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

queryClient exponeras inte längre via tRPC-kontext

tRPC exponerar inte längre queryClient-instansen via trpc.useContext(). Om du behöver använda metoder från queryClient, kontrollera om trpc.useContext() innehåller dem här. Om tRPC ännu inte omsluter den aktuella metoden kan du importera queryClient direkt från @tanstack/react-query:

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

Migrera anpassade felformaterare

Du måste flytta innehållet i din formatError() till din rot-t-router. Se dokumentationen för felformatering för mer information.