サーバーサイドレンダリング
このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
SSRを有効化するには、createTRPCNextの設定コールバックでssr: trueを設定するだけです。
SSRを有効にすると、tRPCはサーバー上ですべてのクエリをプリフェッチするためにgetInitialPropsを使用します。これにより、getServerSidePropsを使用する際にこのような問題が発生する可能性があり、この解決は当方の手に負えません。
代わりに、SSRを無効(デフォルト)のままにし、Server-Side Helpersを使用してgetStaticPropsまたはgetServerSidePropsでクエリをプリフェッチすることもできます。
サーバーサイドレンダリングのステップでクエリを適切に実行するためには、config内に追加ロジックを実装する必要があります。また、Response Cachingも併せてご検討ください。
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssr : true,ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},});
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssr : true,ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},});
あるいは、特定のリクエストに基づいてSSRを条件付きで行いたい場合、ssrにコールバック関数を渡せます。このコールバックはboolean、またはbooleanに解決するPromiseを返します:
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},ssr (opts ) {// only SSR if the request is coming from a botreturnopts .ctx ?.req ?.headers ['user-agent']?.includes ('bot') ?? false;},});
utils/trpc.tstsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from './api/trpc/[trpc]';export consttrpc =createTRPCNext <AppRouter >({ssrPrepass ,config (info ) {const {ctx } =info ;if (typeofwindow !== 'undefined') {// during client requestsreturn {links : [httpBatchLink ({url : '/api/trpc',}),],};}return {links : [httpBatchLink ({// The server needs to know your app's full urlurl : `${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 renderingreturn {cookie :ctx .req .headers .cookie ,};},}),],};},ssr (opts ) {// only SSR if the request is coming from a botreturnopts .ctx ?.req ?.headers ['user-agent']?.includes ('bot') ?? false;},});
pages/_app.tsxtsximport {trpc } from '../utils/trpc';import type {AppProps } from 'next/app';import type {AppType } from 'next/app';constMyApp :AppType = ({Component ,pageProps }:AppProps ) => {return <Component {...pageProps } />;};export defaulttrpc .withTRPC (MyApp );
pages/_app.tsxtsximport {trpc } from '../utils/trpc';import type {AppProps } from 'next/app';import type {AppType } from 'next/app';constMyApp :AppType = ({Component ,pageProps }:AppProps ) => {return <Component {...pageProps } />;};export defaulttrpc .withTRPC (MyApp );
SSRでのレスポンスキャッシュ
アプリでSSRを有効にすると、例えばVercel上でアプリの読み込みが遅くなる可能性がありますが、SSGを使用せずにアプリ全体を静的にレンダリングすることも実際には可能です。詳細な洞察についてはこのTwitterスレッドをお読みください。
createTRPCNextのresponseMetaコールバックを使用してSSRレスポンスのキャッシュヘッダーを設定できます。responseMetaによるフレームワーク非依存のキャッシュ方法については、汎用のResponse Cachingドキュメントも参照してください。
utils/trpc.tsxtsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from '../server/routers/_app';export consttrpc =createTRPCNext <AppRouter >({config () {if (typeofwindow !== 'undefined') {return {links : [httpBatchLink ({url : '/api/trpc',}),],};}consturl =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 callsreturn {status :clientErrors [0].data ?.httpStatus ?? 500,};}// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};},});
utils/trpc.tsxtsximport {httpBatchLink } from '@trpc/client';import {createTRPCNext } from '@trpc/next';import {ssrPrepass } from '@trpc/next/ssrPrepass';import type {AppRouter } from '../server/routers/_app';export consttrpc =createTRPCNext <AppRouter >({config () {if (typeofwindow !== 'undefined') {return {links : [httpBatchLink ({url : '/api/trpc',}),],};}consturl =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 callsreturn {status :clientErrors [0].data ?.httpStatus ?? 500,};}// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};},});
Next.jsアダプターでのAPIレスポンスキャッシュ
Next.js APIハンドラー上でresponseMetaを使用し、APIレスポンスを直接キャッシュすることも可能です:
pages/api/trpc/[trpc].tstsximport {initTRPC } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';export constcreateContext = async ({req ,res ,}:trpcNext .CreateNextContextOptions ) => {return {req ,res ,};};typeContext =Awaited <ReturnType <typeofcreateContext >>;export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({public :t .router ({slowQueryCached :t .procedure .query (async (opts ) => {await newPromise ((resolve ) =>setTimeout (resolve , 5000));return {lastUpdated : newDate ().toJSON (),};}),}),});export typeAppRouter = typeofappRouter ;export defaulttrpcNext .createNextApiHandler ({router :appRouter ,createContext ,responseMeta (opts ) {const {ctx ,paths ,errors ,type } =opts ;// assuming you have all your public routes with the keyword `public` in themconstallPublic =paths &&paths .every ((path ) =>path .includes ('public'));// checking that no procedures erroredconstallOk =errors .length === 0;// checking we're doing a query requestconstisQuery =type === 'query';if (ctx ?.res &&allPublic &&allOk &&isQuery ) {// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['cache-control',`s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS }`,],]),};}return {};},});
pages/api/trpc/[trpc].tstsximport {initTRPC } from '@trpc/server';import * astrpcNext from '@trpc/server/adapters/next';export constcreateContext = async ({req ,res ,}:trpcNext .CreateNextContextOptions ) => {return {req ,res ,};};typeContext =Awaited <ReturnType <typeofcreateContext >>;export constt =initTRPC .context <Context >().create ();export constappRouter =t .router ({public :t .router ({slowQueryCached :t .procedure .query (async (opts ) => {await newPromise ((resolve ) =>setTimeout (resolve , 5000));return {lastUpdated : newDate ().toJSON (),};}),}),});export typeAppRouter = typeofappRouter ;export defaulttrpcNext .createNextApiHandler ({router :appRouter ,createContext ,responseMeta (opts ) {const {ctx ,paths ,errors ,type } =opts ;// assuming you have all your public routes with the keyword `public` in themconstallPublic =paths &&paths .every ((path ) =>path .includes ('public'));// checking that no procedures erroredconstallOk =errors .length === 0;// checking we're doing a query requestconstisQuery =type === 'query';if (ctx ?.res &&allPublic &&allOk &&isQuery ) {// cache request for 1 day + revalidate once every secondconstONE_DAY_IN_SECONDS = 60 * 60 * 24;return {headers : newHeaders ([['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のページを参照してください。