v9からv10への移行
このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
tRPC v10へようこそ!優れた開発者体験(DX)と完璧なエンドツーエンド型安全性を実現する新メジャーバージョンをお届けできることを嬉しく思います。
v10の内部では、パフォーマンス改善の実現、開発体験の向上、そして将来の新機能開発のための基盤構築を行っています。
tRPC v10にはv9からの移行を支援する互換レイヤーが実装されています。.interop()を使用することで段階的にv10へ移行可能で、既存プロジェクトの開発を継続しながらv10の新機能を活用できます。
変更点の概要
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,});
v9からの移行手順
コードベースのアップグレードを今日から開始(そして完了!) するための2つの戦略をご提案します。
コードモッドの利用
@sachinraja がこのメジャーアップグレード向けに優れたコードモッドを開発しました。スクリプトを実行すれば、95%の作業が瞬時に完了します。
- コードモッドを利用する場合でも、完全移行前に以下の手順1-3を実施し動作確認することを推奨します
- このコードモッドは完全ではありませんが、大部分の重労働を代行してくれます
.interop()の活用
既存のv9ルートをすべて書き換えるのは負荷が大きいかもしれません。代わりに、v9のプロシージャを維持したまま、v10のinterop()メソッドを活用して段階的に移行しましょう。
1. v9ルーターでinterop()を有効化
v9ルーターをv10ルーターに変換するのはたった10文字の作業です。v9ルーターの末尾に.interop()を追加するだけでサーバー側コードの移行は完了です!
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;
.interop()がサポートしない機能がいくつか存在します。ほとんどのユーザーは.interop()を使用して数分でサーバーサイドコードを移行できることを期待しています。.interop()が正しく動作していないことが判明した場合は、制限事項を確認してください。
2. tオブジェクトの作成
新規ルートでv10を利用開始するため、v10ルーターを初期化しましょう。
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. 新規appRouterの作成
-
既存の
appRouterをlegacyRouterにリネーム -
新しいアプリケーションルーターを作成:
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;
同一の呼び出し名を持つプロシージャを使用しないよう注意してください!レガシールーターと新規ルーターでパスが重複すると問題が発生します。
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からリンクに移動しました。
カスタムリンク
v10ではリンクアーキテクチャが完全に刷新されました。そのため、v9用のカスタムリンクはv10やinteropでは動作しません。v10用カスタムリンクの作成方法についてはリンクのドキュメントを参照してください。
クライアントパッケージの変更点
v10ではクライアントサイドにも変更が加えられています。いくつかの主要な変更を行うことで、以下の利点が得られます:
- クライアントから直接サーバー定義へジャンプ可能
- クライアントから直接ルーターやプロシージャのリネームが可能
@trpc/react-query
@trpc/reactから@trpc/react-queryへの名称変更
@trpc/reactパッケージは@trpc/react-queryに名称変更されました。これはreact-queryのラッパーであることを明確にし、将来のReact Server Components(RSCs)や他のデータ取得ライブラリアダプターなど、@trpc/react-queryパッケージを使わずにtRPCをReactで使用するケースに対応するためです。@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のメジャーバージョンアップ
peerDependenciesをreact-query@^3から@tanstack/react-query@^4にアップグレードしました。クライアントフックはreact-queryのシンラッパーであるため、React Queryの移行ガイドを参照することをお勧めします。
フックのtRPC固有オプションがtrpcに移動
組み込みのreact-queryプロパティとの衝突や混乱を避けるため、tRPC固有のオプションをtrpcプロパティに移動しました。これによりtRPC固有のオプションが明確になり、将来react-queryとの衝突を防げます。
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,},});
クエリキーの変更
アプリ内でtRPC提供のAPIのみを使用している場合、移行に問題はありません 👍
ただしtanstack query clientを直接使用してqueryClient.setQueriesDataで複数のtRPC生成クエリデータを更新している場合は注意が必要です!
ルーター全体の無効化などの高度な機能実装のため、内部で使用するtanstackクエリキーの仕様を変更する必要がありました。
クエリキーの形式を変更しました。プロシージャパスを.で連結した文字列から、要素のサブ配列へ移行しました。またキャッシュ配置時のqueryとinfiniteクエリの区別を追加し、クエリのtypeと入力値を名前付きプロパティを持つオブジェクトに統合しました。
以下のシンプルなルーターを例に説明します:
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 } })),}),});
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ではWeb標準に合わせてAbortController Web APIを採用しました。.cancel()の代わりに、クエリにAbortSignalを渡し、親となるAbortControllerで.abort()を呼び出します。
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();
HTTP固有オプションの TRPCClient からリンクへの移行
従来HTTPオプション(ヘッダーなど)はcreateTRPCClient()に直接設定されていましたが、tRPCは本質的にHTTPに依存しないため、これらをTRPCClientからhttpLinkおよび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',};},})]});
この変更は@trpc/serverパッケージにも反映され、http関連のエクスポートがメインエントリポイントから@trpc/server/http専用エントリポイントに移動しました。
追加事項
teardownオプションの廃止
teardownオプションは廃止され、利用できなくなりました。
createContextの戻り値型
createContext関数はnullまたはundefinedを返せなくなりました。カスタムコンテキストを使用していない場合、空のオブジェクトを返す必要があります:
diff- createContext: () => null,+ createContext: () => ({}),
diff- createContext: () => null,+ createContext: () => ({}),
tRPCコンテキストからのqueryClient公開終了
tRPCはqueryClientインスタンスをtrpc.useContext()経由で公開しなくなりました。queryClientのメソッドを使用する必要がある場合は、こちらでtrpc.useContext()が提供するラップメソッドを確認してください。必要なメソッドがまだラップされていない場合は、@tanstack/react-queryから直接queryClientをインポートして使用できます:
tsximport { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
tsximport { useQueryClient } from '@tanstack/react-query';const MyComponent = () => {const queryClient = useQueryClient();// ...};
カスタムエラーフォーマッタの移行
formatError()の内容をルートtルーターに移行する必要があります。詳細はエラーフォーマッティングドキュメントを参照してください。