React Server Components でのセットアップ
このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
このガイドは、Next.js App RouterのようなReact Server Components(RSC)フレームワークでtRPCを使用する方法の概要です。 RSC自体がtRPCが解決しようとしていた多くの問題を解決するため、tRPCが全く不要な場合もあることに留意してください。
また、tRPCとRSCを統合する万能な方法は存在しないため、このガイドを出発点として自身のニーズや好みに合わせて調整してください。
tRPCをServer Actionsと使用する方法については、Juliusのブログ記事を参照してください。
進める前に、React Queryの高度なサーバーサイドレンダリングドキュメントを読んで、異なるタイプのサーバーレンダリングと避けるべき落とし穴を理解してください。
既存プロジェクトへのtRPC追加
1. 依存関係のインストール
- npm
- yarn
- pnpm
- bun
- deno
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
yarn add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
pnpm add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
bun add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod client-only server-only
deno add npm:@trpc/server npm:@trpc/client npm:@trpc/tanstack-react-query npm:@tanstack/react-query@latest npm:zod npm:client-only npm:server-only
2. tRPCルーターの作成
trpc/init.tsでinitTRPC関数を使用してtRPCバックエンドを初期化し、最初のルーターを作成します。ここではシンプルな「hello world」ルーターとプロシージャを作成します - tRPC APIの作成に関する詳細な情報については、クイックスタートガイドとバックエンド使用ドキュメントを参照してください。
ここで使用しているファイル名はtRPCによって強制されるものではありません。任意のファイル構造を使用できます。
View sample backend
trpc/init.tstsimport { initTRPC } from '@trpc/server';import { cache } from 'react';export const createTRPCContext = cache(async () => {/*** @see: https://trpc.io/docs/server/context*/return { userId: 'user_123' };});// Avoid exporting the entire t-object// since it's not very descriptive.// For instance, the use of a t variable// is common in i18n libraries.const t = initTRPC.create({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport const createTRPCRouter = t.router;export const createCallerFactory = t.createCallerFactory;export const baseProcedure = t.procedure;
trpc/init.tstsimport { initTRPC } from '@trpc/server';import { cache } from 'react';export const createTRPCContext = cache(async () => {/*** @see: https://trpc.io/docs/server/context*/return { userId: 'user_123' };});// Avoid exporting the entire t-object// since it's not very descriptive.// For instance, the use of a t variable// is common in i18n libraries.const t = initTRPC.create({/*** @see https://trpc.io/docs/server/data-transformers*/// transformer: superjson,});// Base router and procedure helpersexport const createTRPCRouter = t.router;export const createCallerFactory = t.createCallerFactory;export const baseProcedure = t.procedure;
trpc/routers/_app.tstsimport { z } from 'zod';import { baseProcedure, createTRPCRouter } from '../init';export const appRouter = createTRPCRouter({hello: baseProcedure.input(z.object({text: z.string(),}),).query((opts) => {return {greeting: `hello ${opts.input.text}`,};}),});// export type definition of APIexport type AppRouter = typeof appRouter;
trpc/routers/_app.tstsimport { z } from 'zod';import { baseProcedure, createTRPCRouter } from '../init';export const appRouter = createTRPCRouter({hello: baseProcedure.input(z.object({text: z.string(),}),).query((opts) => {return {greeting: `hello ${opts.input.text}`,};}),});// export type definition of APIexport type AppRouter = typeof appRouter;
The backend adapter depends on your framework and how it sets up API routes. The following example sets up GET and POST routes at /api/trpc/* using the fetch adapter in Next.js.
app/api/trpc/[trpc]/route.tstsimport { fetchRequestHandler } from '@trpc/server/adapters/fetch';import { createTRPCContext } from '~/trpc/init';import { appRouter } from '~/trpc/routers/_app';const handler = (req: Request) =>fetchRequestHandler({endpoint: '/api/trpc',req,router: appRouter,createContext: createTRPCContext,});export { handler as GET, handler as POST };
app/api/trpc/[trpc]/route.tstsimport { fetchRequestHandler } from '@trpc/server/adapters/fetch';import { createTRPCContext } from '~/trpc/init';import { appRouter } from '~/trpc/routers/_app';const handler = (req: Request) =>fetchRequestHandler({endpoint: '/api/trpc',req,router: appRouter,createContext: createTRPCContext,});export { handler as GET, handler as POST };
3. Query Clientファクトリーの作成
共有ファイルtrpc/query-client.tsを作成し、QueryClientインスタンスを生成する関数をエクスポートします。
trpc/query-client.tstsimport {defaultShouldDehydrateQuery,QueryClient,} from '@tanstack/react-query';import superjson from 'superjson';export function makeQueryClient() {return new QueryClient({defaultOptions: {queries: {staleTime: 30 * 1000,},dehydrate: {// serializeData: superjson.serialize,shouldDehydrateQuery: (query) =>defaultShouldDehydrateQuery(query) ||query.state.status === 'pending',},hydrate: {// deserializeData: superjson.deserialize,},},});}
trpc/query-client.tstsimport {defaultShouldDehydrateQuery,QueryClient,} from '@tanstack/react-query';import superjson from 'superjson';export function makeQueryClient() {return new QueryClient({defaultOptions: {queries: {staleTime: 30 * 1000,},dehydrate: {// serializeData: superjson.serialize,shouldDehydrateQuery: (query) =>defaultShouldDehydrateQuery(query) ||query.state.status === 'pending',},hydrate: {// deserializeData: superjson.deserialize,},},});}
ここではいくつかのデフォルトオプションを設定しています:
-
staleTime: SSRを使用する場合、クライアントでの即時再取得を避けるため、通常はデフォルトのstaleTimeを0より大きい値に設定します。 -
shouldDehydrateQuery: クエリを脱水処理するかどうかを決定する関数です。RSCトランスポートプロトコルはネットワーク経由でのPromiseのハイドレートをサポートしているため、defaultShouldDehydrateQuery関数を拡張して、まだ保留中のクエリも含めるようにします。これにより、ツリー上位のサーバーコンポーネントでプリフェッチを開始し、そのPromiseを下位のクライアントコンポーネントで消費できるようになります。 -
serializeDataとdeserializeData(オプション): 前の手順でデータトランスフォーマーを設定した場合、サーバー-クライアント境界を越えてクエリクライアントをハイドレートする際にデータが正しくシリアライズされるよう、このオプションを設定します。
4. クライアントコンポーネント向けtRPCクライアントの作成
trpc/client.tsxは、クライアントコンポーネントからtRPC APIを利用する際のエントリポイントです。ここではtRPCルーターの型定義をインポートし、
createTRPCContextを使用して型安全なフックを作成します。また、このファイルからコンテキストプロバイダーをエクスポートします。
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport type { QueryClient } from '@tanstack/react-query';import { QueryClientProvider } from '@tanstack/react-query';import { createTRPCClient, httpBatchLink } from '@trpc/client';import { createTRPCContext } from '@trpc/tanstack-react-query';import { useState } from 'react';import { makeQueryClient } from './query-client';import type { AppRouter } from './routers/_app';export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();let browserQueryClient: QueryClient;function getQueryClient() {if (typeof window === 'undefined') {// Server: always make a new query clientreturn makeQueryClient();}// Browser: make a new query client if we don't already have one// This is very important, so we don't re-make a new client if React// suspends during the initial render. This may not be needed if we// have a suspense boundary BELOW the creation of the query clientif (!browserQueryClient) browserQueryClient = makeQueryClient();return browserQueryClient;}function getUrl() {const base = (() => {if (typeof window !== 'undefined') return '';if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;return 'http://localhost:3000';})();return `${base}/api/trpc`;}export function TRPCReactProvider(props: Readonly<{children: React.ReactNode;}>,) {// NOTE: Avoid useState when initializing the query client if you don't// have a suspense boundary between this and the code that may// suspend because React will throw away the client on the initial// render if it suspends and there is no boundaryconst queryClient = getQueryClient();const [trpcClient] = useState(() =>createTRPCClient<AppRouter>({links: [httpBatchLink({// transformer: superjson, <-- if you use a data transformerurl: getUrl(),}),],}),);return (<QueryClientProvider client={queryClient}><TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>{props.children}</TRPCProvider></QueryClientProvider>);}
trpc/client.tsxtsx'use client';// ^-- to make sure we can mount the Provider from a server componentimport type { QueryClient } from '@tanstack/react-query';import { QueryClientProvider } from '@tanstack/react-query';import { createTRPCClient, httpBatchLink } from '@trpc/client';import { createTRPCContext } from '@trpc/tanstack-react-query';import { useState } from 'react';import { makeQueryClient } from './query-client';import type { AppRouter } from './routers/_app';export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();let browserQueryClient: QueryClient;function getQueryClient() {if (typeof window === 'undefined') {// Server: always make a new query clientreturn makeQueryClient();}// Browser: make a new query client if we don't already have one// This is very important, so we don't re-make a new client if React// suspends during the initial render. This may not be needed if we// have a suspense boundary BELOW the creation of the query clientif (!browserQueryClient) browserQueryClient = makeQueryClient();return browserQueryClient;}function getUrl() {const base = (() => {if (typeof window !== 'undefined') return '';if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;return 'http://localhost:3000';})();return `${base}/api/trpc`;}export function TRPCReactProvider(props: Readonly<{children: React.ReactNode;}>,) {// NOTE: Avoid useState when initializing the query client if you don't// have a suspense boundary between this and the code that may// suspend because React will throw away the client on the initial// render if it suspends and there is no boundaryconst queryClient = getQueryClient();const [trpcClient] = useState(() =>createTRPCClient<AppRouter>({links: [httpBatchLink({// transformer: superjson, <-- if you use a data transformerurl: getUrl(),}),],}),);return (<QueryClientProvider client={queryClient}><TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>{props.children}</TRPCProvider></QueryClientProvider>);}
アプリケーションのルート(例:Next.jsを使用する場合はapp/layout.tsx)にプロバイダーをマウントします。
5. サーバーコンポーネント向けtRPCコーラーの作成
サーバーコンポーネントからクエリをプリフェッチするために、ルーターからプロキシを作成します。ルーターが別サーバーにある場合はクライアントを渡すことも可能です。
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';import { cache } from 'react';import { createTRPCContext } from './init';import { makeQueryClient } from './query-client';import { appRouter } from './routers/_app';// IMPORTANT: Create a stable getter for the query client that// will return the same client during the same request.export const getQueryClient = cache(makeQueryClient);export const trpc = createTRPCOptionsProxy({ctx: createTRPCContext,router: appRouter,queryClient: getQueryClient,});// If your router is on a separate server, pass a client:createTRPCOptionsProxy({client: createTRPCClient({links: [httpLink({ url: '...' })],}),queryClient: getQueryClient,});
trpc/server.tsxtsximport 'server-only'; // <-- ensure this file cannot be imported from the clientimport { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';import { cache } from 'react';import { createTRPCContext } from './init';import { makeQueryClient } from './query-client';import { appRouter } from './routers/_app';// IMPORTANT: Create a stable getter for the query client that// will return the same client during the same request.export const getQueryClient = cache(makeQueryClient);export const trpc = createTRPCOptionsProxy({ctx: createTRPCContext,router: appRouter,queryClient: getQueryClient,});// If your router is on a separate server, pass a client:createTRPCOptionsProxy({client: createTRPCClient({links: [httpLink({ url: '...' })],}),queryClient: getQueryClient,});
APIの使用方法
これでアプリ内でtRPC APIを使用できます。クライアントコンポーネントでは他のReactアプリと同様にReact Queryフックを使用できますが、
RSCの機能を活用するにはツリー上位のサーバーコンポーネントでクエリをプリフェッチできます。これは「レンダリングしながらフェッチ」する
ローダーとして実装される一般的な概念で、リクエストを可能な限り早く開始しつつ、useQueryまたはuseSuspenseQueryフックを使用して
データが必要になるまでサスペンドしないことを意味します。
このアプローチはNext.js App Routerのストリーミング機能を活用し、サーバーでクエリを開始してデータが利用可能になり次第クライアントにストリーミングします。
ブラウザの初回バイト到達時間とデータ取得時間の両方を最適化し、ページ読み込みを高速化します。
ただし、データがストリーミングされる前はgreeting.dataが初期状態でundefinedになる可能性があります。
この初期のundefined状態を避けたい場合は、prefetchQuery呼び出しをawaitできます。
これによりクライアントのクエリは初回レンダリング時に常にデータを持つようになりますが、トレードオフとして
サーバーがクエリを完了してからHTMLをクライアントに送信するため、ページ読み込みが遅くなります。
app/page.tsxtsximport { dehydrate, HydrationBoundary } from '@tanstack/react-query';import { getQueryClient, trpc } from '~/trpc/server';import { ClientGreeting } from './client-greeting';export default async function Home() {const queryClient = getQueryClient();void queryClient.prefetchQuery(trpc.hello.queryOptions({/** input */}),);return (<HydrationBoundary state={dehydrate(queryClient)}><div>...</div>{/** ... */}<ClientGreeting /></HydrationBoundary>);}
app/page.tsxtsximport { dehydrate, HydrationBoundary } from '@tanstack/react-query';import { getQueryClient, trpc } from '~/trpc/server';import { ClientGreeting } from './client-greeting';export default async function Home() {const queryClient = getQueryClient();void queryClient.prefetchQuery(trpc.hello.queryOptions({/** input */}),);return (<HydrationBoundary state={dehydrate(queryClient)}><div>...</div>{/** ... */}<ClientGreeting /></HydrationBoundary>);}
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport { useQuery } from '@tanstack/react-query';import { useTRPC } from '~/trpc/client';export function ClientGreeting() {const trpc = useTRPC();const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));if (!greeting.data) return <div>Loading...</div>;return <div>{greeting.data.greeting}</div>;}
app/client-greeting.tsxtsx'use client';// <-- hooks can only be used in client componentsimport { useQuery } from '@tanstack/react-query';import { useTRPC } from '~/trpc/client';export function ClientGreeting() {const trpc = useTRPC();const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' }));if (!greeting.data) return <div>Loading...</div>;return <div>{greeting.data.greeting}</div>;}
prefetchとHydrateClientヘルパー関数を作成すると、より簡潔で再利用可能になります:
trpc/server.tsxtsxexport function HydrateClient(props: { children: React.ReactNode }) {const queryClient = getQueryClient();return (<HydrationBoundary state={dehydrate(queryClient)}>{props.children}</HydrationBoundary>);}export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(queryOptions: T,) {const queryClient = getQueryClient();if (queryOptions.queryKey[1]?.type === 'infinite') {void queryClient.prefetchInfiniteQuery(queryOptions as any);} else {void queryClient.prefetchQuery(queryOptions);}}
trpc/server.tsxtsxexport function HydrateClient(props: { children: React.ReactNode }) {const queryClient = getQueryClient();return (<HydrationBoundary state={dehydrate(queryClient)}>{props.children}</HydrationBoundary>);}export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(queryOptions: T,) {const queryClient = getQueryClient();if (queryOptions.queryKey[1]?.type === 'infinite') {void queryClient.prefetchInfiniteQuery(queryOptions as any);} else {void queryClient.prefetchQuery(queryOptions);}}
以下のように使用できます:
tsximport { HydrateClient, prefetch, trpc } from '~/trpc/server';function Home() {prefetch(trpc.hello.queryOptions({/** input */}),);return (<HydrateClient><div>...</div>{/** ... */}<ClientGreeting /></HydrateClient>);}
tsximport { HydrateClient, prefetch, trpc } from '~/trpc/server';function Home() {prefetch(trpc.hello.queryOptions({/** input */}),);return (<HydrateClient><div>...</div>{/** ... */}<ClientGreeting /></HydrateClient>);}
Suspenseの活用
SuspenseとError Boundariesを使用してローディング状態とエラー状態を処理することをお勧めします。これはuseSuspenseQueryフックを使用して実現できます。
app/page.tsxtsximport { HydrateClient, prefetch, trpc } from '~/trpc/server';import { Suspense } from 'react';import { ErrorBoundary } from 'react-error-boundary';import { ClientGreeting } from './client-greeting';export default async function Home() {prefetch(trpc.hello.queryOptions());return (<HydrateClient><div>...</div>{/** ... */}<ErrorBoundary fallback={<div>Something went wrong</div>}><Suspense fallback={<div>Loading...</div>}><ClientGreeting /></Suspense></ErrorBoundary></HydrateClient>);}
app/page.tsxtsximport { HydrateClient, prefetch, trpc } from '~/trpc/server';import { Suspense } from 'react';import { ErrorBoundary } from 'react-error-boundary';import { ClientGreeting } from './client-greeting';export default async function Home() {prefetch(trpc.hello.queryOptions());return (<HydrateClient><div>...</div>{/** ... */}<ErrorBoundary fallback={<div>Something went wrong</div>}><Suspense fallback={<div>Loading...</div>}><ClientGreeting /></Suspense></ErrorBoundary></HydrateClient>);}
app/client-greeting.tsxtsx'use client';import { useSuspenseQuery } from '@tanstack/react-query';import { trpc } from '~/trpc/client';export function ClientGreeting() {const trpc = useTRPC();const { data } = useSuspenseQuery(trpc.hello.queryOptions());return <div>{data.greeting}</div>;}
app/client-greeting.tsxtsx'use client';import { useSuspenseQuery } from '@tanstack/react-query';import { trpc } from '~/trpc/client';export function ClientGreeting() {const trpc = useTRPC();const { data } = useSuspenseQuery(trpc.hello.queryOptions());return <div>{data.greeting}</div>;}
サーバーコンポーネントでのデータ取得
サーバーコンポーネントでデータにアクセスする必要がある場合は、サーバーコーラーを作成して直接使用することを推奨します。この方法はクエリクライアントから切り離されており、 データをキャッシュに保存しないことに注意してください。つまりサーバーコンポーネントで使用したデータがクライアントで利用可能になることは期待できません。これは意図的な設計で、 詳細は高度なサーバーサイドレンダリング ガイドで説明されています。
trpc/server.tsxtsx// ...export const caller = appRouter.createCaller(createTRPCContext);
trpc/server.tsxtsx// ...export const caller = appRouter.createCaller(createTRPCContext);
app/page.tsxtsximport { caller } from '~/trpc/server';export default async function Home() {const greeting = await caller.hello();// ^? { greeting: string }return <div>{greeting.greeting}</div>;}
app/page.tsxtsximport { caller } from '~/trpc/server';export default async function Home() {const greeting = await caller.hello();// ^? { greeting: string }return <div>{greeting.greeting}</div>;}
サーバーとクライアントコンポーネントの両方でデータを使用する必要が本当にあり、高度なサーバーサイドレンダリングガイドで説明されているトレードオフを理解している場合、prefetchの代わりにfetchQueryを使用することで、サーバー上にデータを持たせつつ、クライアント側にハイドレートして渡すことができます:
app/page.tsxtsximport { getQueryClient, HydrateClient, trpc } from '~/trpc/server';export default async function Home() {const queryClient = getQueryClient();const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());// Do something with greeting on the serverreturn (<HydrateClient><div>...</div>{/** ... */}<ClientGreeting /></HydrateClient>);}
app/page.tsxtsximport { getQueryClient, HydrateClient, trpc } from '~/trpc/server';export default async function Home() {const queryClient = getQueryClient();const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions());// Do something with greeting on the serverreturn (<HydrateClient><div>...</div>{/** ... */}<ClientGreeting /></HydrateClient>);}