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

サーバーサイドレンダリング

非公式ベータ版翻訳

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

SSRを有効化するには、createTRPCNextの設定コールバックでssr: trueを設定するだけです。

情報

SSRを有効にすると、tRPCはサーバー上ですべてのクエリをプリフェッチするためにgetInitialPropsを使用します。これにより、getServerSidePropsを使用する際にこのような問題が発生する可能性があり、この解決は当方の手に負えません。

 
代わりに、SSRを無効(デフォルト)のままにし、Server-Side Helpersを使用してgetStaticPropsまたはgetServerSidePropsでクエリをプリフェッチすることもできます。

サーバーサイドレンダリングのステップでクエリを適切に実行するためには、config内に追加ロジックを実装する必要があります。また、Response Cachingも併せてご検討ください。

utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssr: true,
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
});
utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssr: true,
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
});

あるいは、特定のリクエストに基づいてSSRを条件付きで行いたい場合、ssrにコールバック関数を渡せます。このコールバックはboolean、またはbooleanに解決するPromiseを返します:

utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
ssr(opts) {
// only SSR if the request is coming from a bot
return opts.ctx?.req?.headers['user-agent']?.includes('bot') ?? false;
},
});
utils/trpc.ts
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from './api/trpc/[trpc]';
 
export const trpc = createTRPCNext<AppRouter>({
ssrPrepass,
config(info) {
const { ctx } = info;
if (typeof window !== 'undefined') {
// during client requests
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
return {
links: [
httpBatchLink({
// The server needs to know your app's full url
url: `${getBaseUrl()}/api/trpc`,
/**
* Set custom request headers on every request from tRPC
* @see https://trpc.io/docs/client/headers
*/
headers() {
if (!ctx?.req?.headers) {
return {};
}
// To use SSR properly, you need to forward client headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
return {
cookie: ctx.req.headers.cookie,
};
},
}),
],
};
},
ssr(opts) {
// only SSR if the request is coming from a bot
return opts.ctx?.req?.headers['user-agent']?.includes('bot') ?? false;
},
});
pages/_app.tsx
tsx
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';
import type { AppType } from 'next/app';
 
const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
 
export default trpc.withTRPC(MyApp);
pages/_app.tsx
tsx
import { trpc } from '../utils/trpc';
import type { AppProps } from 'next/app';
import type { AppType } from 'next/app';
 
const MyApp: AppType = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
 
export default trpc.withTRPC(MyApp);

SSRでのレスポンスキャッシュ

アプリでSSRを有効にすると、例えばVercel上でアプリの読み込みが遅くなる可能性がありますが、SSGを使用せずにアプリ全体を静的にレンダリングすることも実際には可能です。詳細な洞察についてはこのTwitterスレッドをお読みください。

createTRPCNextresponseMetaコールバックを使用してSSRレスポンスのキャッシュヘッダーを設定できます。responseMetaによるフレームワーク非依存のキャッシュ方法については、汎用のResponse Cachingドキュメントも参照してください。

utils/trpc.tsx
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';
 
export const trpc = createTRPCNext<AppRouter>({
config() {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
 
return {
links: [
httpBatchLink({
url,
}),
],
};
},
ssr: true,
ssrPrepass,
responseMeta(opts) {
const { clientErrors } = opts;
 
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
 
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
},
});
utils/trpc.tsx
tsx
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import { ssrPrepass } from '@trpc/next/ssrPrepass';
import type { AppRouter } from '../server/routers/_app';
 
export const trpc = createTRPCNext<AppRouter>({
config() {
if (typeof window !== 'undefined') {
return {
links: [
httpBatchLink({
url: '/api/trpc',
}),
],
};
}
 
const url = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: 'http://localhost:3000/api/trpc';
 
return {
links: [
httpBatchLink({
url,
}),
],
};
},
ssr: true,
ssrPrepass,
responseMeta(opts) {
const { clientErrors } = opts;
 
if (clientErrors.length) {
// propagate http first error from API calls
return {
status: clientErrors[0].data?.httpStatus ?? 500,
};
}
 
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
},
});

Next.jsアダプターでのAPIレスポンスキャッシュ

Next.js APIハンドラー上でresponseMetaを使用し、APIレスポンスを直接キャッシュすることも可能です:

pages/api/trpc/[trpc].ts
tsx
import { initTRPC } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
 
export const createContext = async ({
req,
res,
}: trpcNext.CreateNextContextOptions) => {
return {
req,
res,
};
};
 
type Context = Awaited<ReturnType<typeof createContext>>;
 
export const t = initTRPC.context<Context>().create();
 
export const appRouter = t.router({
public: t.router({
slowQueryCached: t.procedure.query(async (opts) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
 
return {
lastUpdated: new Date().toJSON(),
};
}),
}),
});
 
export type AppRouter = typeof appRouter;
 
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
responseMeta(opts) {
const { ctx, paths, errors, type } = opts;
// assuming you have all your public routes with the keyword `public` in them
const allPublic = paths && paths.every((path) => path.includes('public'));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === 'query';
 
if (ctx?.res && allPublic && allOk && isQuery) {
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});
pages/api/trpc/[trpc].ts
tsx
import { initTRPC } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
 
export const createContext = async ({
req,
res,
}: trpcNext.CreateNextContextOptions) => {
return {
req,
res,
};
};
 
type Context = Awaited<ReturnType<typeof createContext>>;
 
export const t = initTRPC.context<Context>().create();
 
export const appRouter = t.router({
public: t.router({
slowQueryCached: t.procedure.query(async (opts) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
 
return {
lastUpdated: new Date().toJSON(),
};
}),
}),
});
 
export type AppRouter = typeof appRouter;
 
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
responseMeta(opts) {
const { ctx, paths, errors, type } = opts;
// assuming you have all your public routes with the keyword `public` in them
const allPublic = paths && paths.every((path) => path.includes('public'));
// checking that no procedures errored
const allOk = errors.length === 0;
// checking we're doing a query request
const isQuery = type === 'query';
 
if (ctx?.res && allPublic && allOk && isQuery) {
// cache request for 1 day + revalidate once every second
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
return {
headers: new Headers([
[
'cache-control',
`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`,
],
]),
};
}
return {};
},
});

よくある質問

Q: クライアントのヘッダーをサーバーに手動で転送する必要があるのはなぜですか? tRPCが自動で転送しないのはなぜですか?

SSR実行時にクライアントのヘッダーをサーバーに転送したくないケースは稀ですが、ヘッダーに動的に要素を追加したい場合があるからです。そのためtRPCは、ヘッダーキーの衝突などの責任を負いたくありません。

Q: Node 18でSSRを使用する際、なぜconnectionヘッダーを削除する必要があるのですか?

connectionヘッダーを削除しない場合、データ取得がTRPCClientError: fetch failedで失敗します。これはconnection禁止ヘッダー名だからです。

Q: ネットワークタブに依然としてリクエストが表示されるのはなぜですか?

デフォルトでは、データ取得フックに使用している@tanstack/react-queryは、SSR経由で初期データを取得済みの場合でも、マウント時やウィンドウのフォーカス時にデータを再取得します。これによりデータが常に最新状態に保たれます。この動作を無効化したい場合は、SSGのページを参照してください。