Server-Side Rendering
To enable SSR just set ssr: true
in your createTRPCNext
config callback.
When you enable SSR, tRPC will use getInitialProps
to prefetch all queries on the server. This results in problems like this when you use getServerSideProps
, and solving it is out of our hands.
Alternatively, you can leave SSR disabled (the default) and use Server-Side Helpers to prefetch queries in getStaticProps
or getServerSideProps
.
In order to execute queries properly during the server-side render step we need to add extra logic inside our config
:
Additionally, consider Response Caching
.
utils/trpc.tstsx
import { httpBatchLink } from '@trpc/client';import { createTRPCNext } from '@trpc/next';import superjson from 'superjson';import type { AppRouter } from './api/trpc/[trpc]';export const trpc = createTRPCNext<AppRouter>({config(opts) {const { ctx } = opts;if (typeof window !== 'undefined') {// during client requestsreturn {transformer: superjson, // optional - adds superjson serializationlinks: [httpBatchLink({url: '/api/trpc',}),],};}return {transformer: superjson, // optional - adds superjson serializationlinks: [httpBatchLink({// The server needs to know your app's full urlurl: `${getBaseUrl()}/api/trpc`,/*** Set custom request headers on every request from tRPC* @link https://trpc.io/docs/v10/header*/headers() {if (!ctx?.req?.headers) {return {};}// To use SSR properly, you need to forward the client's headers to the server// This is so you can pass through things like cookies when we're server-side renderingconst {// If you're using Node 18 before 18.15.0, omit the "connection" headerconnection: _connection,...headers} = ctx.req.headers;return headers;},}),],};},ssr: true,});
utils/trpc.tstsx
import { httpBatchLink } from '@trpc/client';import { createTRPCNext } from '@trpc/next';import superjson from 'superjson';import type { AppRouter } from './api/trpc/[trpc]';export const trpc = createTRPCNext<AppRouter>({config(opts) {const { ctx } = opts;if (typeof window !== 'undefined') {// during client requestsreturn {transformer: superjson, // optional - adds superjson serializationlinks: [httpBatchLink({url: '/api/trpc',}),],};}return {transformer: superjson, // optional - adds superjson serializationlinks: [httpBatchLink({// The server needs to know your app's full urlurl: `${getBaseUrl()}/api/trpc`,/*** Set custom request headers on every request from tRPC* @link https://trpc.io/docs/v10/header*/headers() {if (!ctx?.req?.headers) {return {};}// To use SSR properly, you need to forward the client's headers to the server// This is so you can pass through things like cookies when we're server-side renderingconst {// If you're using Node 18 before 18.15.0, omit the "connection" headerconnection: _connection,...headers} = ctx.req.headers;return headers;},}),],};},ssr: true,});
pages/_app.tsxtsx
import type { AppProps } from 'next/app';import React from 'react';import { trpc } from '~/utils/trpc';const MyApp: AppType = ({ Component, pageProps }: AppProps) => {return <Component {...pageProps} />;};export default trpc.withTRPC(MyApp);
pages/_app.tsxtsx
import type { AppProps } from 'next/app';import React from 'react';import { trpc } from '~/utils/trpc';const MyApp: AppType = ({ Component, pageProps }: AppProps) => {return <Component {...pageProps} />;};export default trpc.withTRPC(MyApp);
Using server-side helpers
utils/trpc.tstsx
import { httpBatchLink } from '@trpc/client';import { createTRPCNext } from '@trpc/next';import superjson from 'superjson';import type { AppRouter } from './api/trpc/[trpc]';export const trpc = createTRPCNext<AppRouter>({config(opts) {return {transformer: superjson, // optional - adds superjson serializationlinks: [httpBatchLink({url: `${getBaseUrl()}/api/trpc`,}),],},},});
utils/trpc.tstsx
import { httpBatchLink } from '@trpc/client';import { createTRPCNext } from '@trpc/next';import superjson from 'superjson';import type { AppRouter } from './api/trpc/[trpc]';export const trpc = createTRPCNext<AppRouter>({config(opts) {return {transformer: superjson, // optional - adds superjson serializationlinks: [httpBatchLink({url: `${getBaseUrl()}/api/trpc`,}),],},},});
pages/_app.tsxtsx
import type { AppProps } from 'next/app';import React from 'react';import { trpc } from '~/utils/trpc';const MyApp: AppType = ({ Component, pageProps }: AppProps) => {return <Component {...pageProps} />;};export default trpc.withTRPC(MyApp);
pages/_app.tsxtsx
import type { AppProps } from 'next/app';import React from 'react';import { trpc } from '~/utils/trpc';const MyApp: AppType = ({ Component, pageProps }: AppProps) => {return <Component {...pageProps} />;};export default trpc.withTRPC(MyApp);
pages/posts/[id].tsxtsx
import { createServerSideHelpers } from '@trpc/react-query/server';import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';import { prisma } from 'server/context';import { appRouter } from 'server/routers/_app';import superjson from 'superjson';import { trpc } from 'utils/trpc';export async function getServerSideProps(context: GetServerSidePropsContext<{ id: string }>,) {const helpers = createServerSideHelpers({router: appRouter,ctx: {},transformer: superjson, // optional - adds superjson serialization});const id = context.params?.id as string;// check if post exists - `prefetch` doesn't change its behavior// based on the result of the query (including throws), so if we// want to change the logic here in gSSP, we need to use `fetch`.if (helpers.post.exists.fetch({ id })) {// prefetch `post.byId`await helpers.post.byId.prefetch({ id });} else {// if post doesn't exist, return 404return {props: { id },notFound: true,};}return {props: {trpcState: helpers.dehydrate(),id,},};}export default function PostViewPage(props: InferGetServerSidePropsType<typeof getServerSideProps>,) {const { id } = props;const postQuery = trpc.post.byId.useQuery({ id });if (postQuery.status !== 'success') {// won't happen since the query has been prefetchedreturn <>Loading...</>;}const { data } = postQuery;return (<><h1>{data.title}</h1><em>Created {data.createdAt.toLocaleDateString('en-us')}</em><p>{data.text}</p><h2>Raw data:</h2><pre>{JSON.stringify(data, null, 4)}</pre></>);}
pages/posts/[id].tsxtsx
import { createServerSideHelpers } from '@trpc/react-query/server';import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';import { prisma } from 'server/context';import { appRouter } from 'server/routers/_app';import superjson from 'superjson';import { trpc } from 'utils/trpc';export async function getServerSideProps(context: GetServerSidePropsContext<{ id: string }>,) {const helpers = createServerSideHelpers({router: appRouter,ctx: {},transformer: superjson, // optional - adds superjson serialization});const id = context.params?.id as string;// check if post exists - `prefetch` doesn't change its behavior// based on the result of the query (including throws), so if we// want to change the logic here in gSSP, we need to use `fetch`.if (helpers.post.exists.fetch({ id })) {// prefetch `post.byId`await helpers.post.byId.prefetch({ id });} else {// if post doesn't exist, return 404return {props: { id },notFound: true,};}return {props: {trpcState: helpers.dehydrate(),id,},};}export default function PostViewPage(props: InferGetServerSidePropsType<typeof getServerSideProps>,) {const { id } = props;const postQuery = trpc.post.byId.useQuery({ id });if (postQuery.status !== 'success') {// won't happen since the query has been prefetchedreturn <>Loading...</>;}const { data } = postQuery;return (<><h1>{data.title}</h1><em>Created {data.createdAt.toLocaleDateString('en-us')}</em><p>{data.text}</p><h2>Raw data:</h2><pre>{JSON.stringify(data, null, 4)}</pre></>);}
FAQ
Q: Why do I need to forward the client's headers to the server manually? Why doesn't tRPC automatically do that for me?
While it's rare that you wouldn't want to forward the client's headers to the server when doing SSR, you might want to add things dynamically in the headers. Therefore, tRPC doesn't want to take responsibility for header keys colliding, etc.
Q: Why do I need to delete the connection
header when using SSR on Node 18?
If you don't remove the connection
header, the data fetching will fail with TRPCClientError: fetch failed
because connection
is a forbidden header name.
Q: Why do I still see network requests being made in the Network tab?
By default, @tanstack/react-query
(which we use for the data fetching hooks) refetches data on mount and window refocus, even if it's already got initial data via SSR. This ensures data is always up-to-date. See the page on SSG if you'd like to disable this behavior.