メインコンテンツへスキップ
バージョン: 11.x

React Server Componentsでのセットアップ

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

ヒント

これは「クラシック」なReact Query統合のドキュメントです(現在もサポートされていますが)、TanStack React Queryを使用した新しいtRPCプロジェクトを始める際の推奨方法ではありません。代わりに新しいTanStack React Query統合を使用することをお勧めします。

このガイドは、Next.js App RouterのようなReact Server Components(RSC)フレームワークでtRPCを使用する方法の概要です。 RSC自体がtRPCが解決しようとしていた多くの問題を解決するため、tRPCが全く不要な場合もあることに留意してください。

また、tRPCとRSCを統合する万能な方法は存在しないため、このガイドを出発点として自身のニーズや好みに合わせて調整してください。

情報

tRPCをServer Actionsと使用する方法については、Juliusのブログ記事を参照してください。

注意

進める前に、React Queryの高度なサーバーサイドレンダリングドキュメントを読んで、異なるタイプのサーバーレンダリングと避けるべき落とし穴を理解してください。

既存プロジェクトへのtRPC追加

1. 依存関係のインストール

npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query@latest zod client-only server-only

2. tRPCルーターの作成

trpc/init.tsinitTRPC関数を使用してtRPCバックエンドを初期化し、最初のルーターを作成します。ここではシンプルな「hello world」ルーターとプロシージャを作成します - tRPC APIの作成に関する詳細な情報については、クイックスタートガイドバックエンド使用ドキュメントを参照してください。

情報

ここで使用しているファイル名はtRPCによって強制されるものではありません。任意のファイル構造を使用できます。

View sample backend
trpc/init.ts
ts
import { 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 helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
trpc/init.ts
ts
import { 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 helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;

trpc/routers/_app.ts
ts
import { 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 API
export type AppRouter = typeof appRouter;
trpc/routers/_app.ts
ts
import { 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 API
export 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.ts
ts
import { 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.ts
ts
import { 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.ts
ts
import {
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.ts
ts
import {
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を下位のクライアントコンポーネントで消費できるようになります。

  • serializeDatadeserializeData(オプション): 前の手順でデータトランスフォーマーを設定した場合、サーバー-クライアント境界を越えてクエリクライアントをハイドレートする際にデータが正しくシリアライズされるよう、このオプションを設定します。

4. クライアントコンポーネント向けtRPCクライアントの作成

trpc/client.tsxはクライアントコンポーネントからtRPC APIを利用する際のエントリーポイントです。ここでtRPCルーターの型定義をインポートし、createTRPCReactを使用して型安全なフックを作成します。また、このファイルからコンテキストプロバイダーをエクスポートします。

trpc/client.tsx
tsx
'use client';
// ^-- to make sure we can mount the Provider from a server component
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
export const trpc = createTRPCReact<AppRouter>();
let clientQueryClientSingleton: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient();
}
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= makeQueryClient());
}
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 TRPCProvider(
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 boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</trpc.Provider>
);
}
trpc/client.tsx
tsx
'use client';
// ^-- to make sure we can mount the Provider from a server component
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
export const trpc = createTRPCReact<AppRouter>();
let clientQueryClientSingleton: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient();
}
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= makeQueryClient());
}
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 TRPCProvider(
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 boundary
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
// transformer: superjson, <-- if you use a data transformer
url: getUrl(),
}),
],
}),
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</trpc.Provider>
);
}

アプリケーションのルート(例:Next.jsを使用する場合はapp/layout.tsx)にプロバイダーをマウントします。

5. サーバーコンポーネント向けtRPCコーラーの作成

サーバーコンポーネントからクエリをプリフェッチするには、tRPCコーラーを使用します。@trpc/react-query/rscモジュールは、React Queryクライアントと統合するcreateCallerのシンラッパーをエクスポートします。

trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { createHydrationHelpers } from '@trpc/react-query/rsc';
import { cache } from 'react';
import { createCallerFactory, 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);
const caller = createCallerFactory(appRouter)(createTRPCContext);
export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(
caller,
getQueryClient,
);
trpc/server.tsx
tsx
import 'server-only'; // <-- ensure this file cannot be imported from the client
import { createHydrationHelpers } from '@trpc/react-query/rsc';
import { cache } from 'react';
import { createCallerFactory, 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);
const caller = createCallerFactory(appRouter)(createTRPCContext);
export const { trpc, HydrateClient } = createHydrationHelpers<typeof appRouter>(
caller,
getQueryClient,
);

APIの使用方法

これでアプリ内でtRPC APIを使用できます。クライアントコンポーネントでは他のReactアプリと同様にReact Queryフックを使用できますが、 RSCの機能を活用するにはツリー上位のサーバーコンポーネントでクエリをプリフェッチできます。これは「レンダリングしながらフェッチ」する ローダーとして実装される一般的な概念で、リクエストを可能な限り早く開始しつつ、useQueryまたはuseSuspenseQueryフックを使用して データが必要になるまでサスペンドしないことを意味します。

app/page.tsx
tsx
import { trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
void trpc.hello.prefetch();
return (
<HydrateClient>
<div>...</div>
{/** ... */}
<ClientGreeting />
</HydrateClient>
);
}
app/page.tsx
tsx
import { trpc } from '~/trpc/server';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
void trpc.hello.prefetch();
return (
<HydrateClient>
<div>...</div>
{/** ... */}
<ClientGreeting />
</HydrateClient>
);
}
app/client-greeting.tsx
tsx
'use client';
// <-- hooks can only be used in client components
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
const greeting = trpc.hello.useQuery();
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
// <-- hooks can only be used in client components
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
const greeting = trpc.hello.useQuery();
if (!greeting.data) return <div>Loading...</div>;
return <div>{greeting.data.greeting}</div>;
}

Suspenseの活用

SuspenseとError Boundariesを使用してローディング状態とエラー状態を処理することをお勧めします。これはuseSuspenseQueryフックを使用して実現できます。

app/page.tsx
tsx
import { trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
void trpc.hello.prefetch();
return (
<HydrateClient>
<div>...</div>
{/** ... */}
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/page.tsx
tsx
import { trpc } from '~/trpc/server';
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ClientGreeting } from './client-greeting';
export default async function Home() {
void trpc.hello.prefetch();
return (
<HydrateClient>
<div>...</div>
{/** ... */}
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<ClientGreeting />
</Suspense>
</ErrorBoundary>
</HydrateClient>
);
}
app/client-greeting.tsx
tsx
'use client';
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
const [data] = trpc.hello.useSuspenseQuery();
return <div>{data.greeting}</div>;
}
app/client-greeting.tsx
tsx
'use client';
import { trpc } from '~/trpc/client';
export function ClientGreeting() {
const [data] = trpc.hello.useSuspenseQuery();
return <div>{data.greeting}</div>;
}

サーバーコンポーネントでのデータ取得

サーバーコンポーネント内でデータにアクセスする必要がある場合、通常のサーバー呼び出しと同様に.prefetch()ではなく手続きを直接呼び出せます。 ただしこの方法はクエリクライアントから切り離されており、キャッシュにデータを保存しないことに注意してください。つまりサーバーコンポーネントで使用したデータがクライアント側で利用可能になることは期待できません。 この設計は意図的なものであり、詳細は高度なサーバーサイドレンダリングガイドで説明されています。

app/page.tsx
tsx
import { trpc } from '~/trpc/server';
export default async function Home() {
// Use the caller directly without using `.prefetch()`
const greeting = await trpc.hello();
// ^? { greeting: string }
return <div>{greeting.greeting}</div>;
}
app/page.tsx
tsx
import { trpc } from '~/trpc/server';
export default async function Home() {
// Use the caller directly without using `.prefetch()`
const greeting = await trpc.hello();
// ^? { greeting: string }
return <div>{greeting.greeting}</div>;
}