Static Site Generation
On this page
Static site generation requires executing tRPC queries inside getStaticProps on each page.
This can be done using server-side helpers to prefetch the queries, dehydrate them, and pass it to the page. The queries will then automatically pick up the trpcState and use it as an initial value.
Fetch data in getStaticProps
import { function createServerSideHelpers<TRouter extends AnyRouter>(opts: CreateServerSideHelpersOptions<TRouter>): ProtectedIntersection<{
queryClient: QueryClient;
dehydrate: (opts?: DehydrateOptions) => DehydratedState;
}, DecoratedProcedureSSGRecord<TRouter["_def"]["_config"]["$types"], TRouter["_def"]["record"]>>
createServerSideHelpers } from '@trpc/react-query/server';
import { const prisma: {
post: {
findMany: (opts: {
select: {
id: true;
};
}) => Promise<{
id: string;
}[]>;
};
}
prisma } from './server/context';
import { const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter } from './server/routers/_app';
import { const trpc: CreateTRPCReactBase<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, unknown> & DecorateRouterRecord<...>
trpc } from './utils/trpc';
import {
type GetStaticPaths<Params extends ParsedUrlQuery = ParsedUrlQuery> = (context: GetStaticPathsContext) => Promise<GetStaticPathsResult<Params>> | GetStaticPathsResult<Params>GetStaticPaths,
type GetStaticPropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
params?: Params;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
locale?: string;
locales?: string[];
defaultLocale?: string;
revalidateReason?: "on-demand" | "build" | "stale";
}
GetStaticPropsContext,
type InferGetStaticPropsType<T extends (args: any) => any> = Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"]
InferGetStaticPropsType,
} from 'next';
import class superjsonsuperjson from 'superjson';
export async function function getStaticProps(context: GetStaticPropsContext<{
id: string;
}>): Promise<{
props: {
trpcState: DehydratedState;
id: string;
};
revalidate: number;
}>
getStaticProps(
context: GetStaticPropsContext<{
id: string;
}>
context: type GetStaticPropsContext<Params extends ParsedUrlQuery = ParsedUrlQuery, Preview extends PreviewData = PreviewData> = {
params?: Params;
preview?: boolean;
previewData?: Preview;
draftMode?: boolean;
locale?: string;
locales?: string[];
defaultLocale?: string;
revalidateReason?: "on-demand" | "build" | "stale";
}
GetStaticPropsContext<{ id: stringid: string }>,
) {
const const helpers: {
queryClient: QueryClient;
dehydrate: (opts?: DehydrateOptions) => DehydratedState;
} & DecoratedProcedureSSGRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
helpers = createServerSideHelpers<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>>(opts: CreateServerSideHelpersOptions<...>): {
...;
} & DecoratedProcedureSSGRecord<...>
createServerSideHelpers({
router: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
router: const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
appRouter,
ctx: objectctx: {},
transformer: DataTransformerOptionstransformer: class superjsonsuperjson, // optional - adds superjson serialization
});
const const id: stringid = context: GetStaticPropsContext<{
id: string;
}>
context.params?: {
id: string;
} | undefined
params?.id: string | undefinedid as string;
// prefetch `post.byId`
await const helpers: {
queryClient: QueryClient;
dehydrate: (opts?: DehydrateOptions) => DehydratedState;
} & DecoratedProcedureSSGRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
helpers.post: DecoratedProcedureSSGRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
post.byId: Pick<DecorateQueryProcedure<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>>, SSGFns>
byId.function prefetch(input: {
id: string;
}, opts?: TRPCFetchQueryOptions<{
id: string;
title: string;
text: string;
createdAt: Date;
}, TRPCClientError<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}>> | undefined): Promise<void>
prefetch({ id: stringid });
return {
props: {
trpcState: DehydratedState;
id: string;
}
props: {
trpcState: DehydratedStatetrpcState: const helpers: {
queryClient: QueryClient;
dehydrate: (opts?: DehydrateOptions) => DehydratedState;
} & DecoratedProcedureSSGRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>
helpers.dehydrate: (opts?: DehydrateOptions) => DehydratedStatedehydrate(),
id: stringid,
},
revalidate: numberrevalidate: 1,
};
}
export const const getStaticPaths: GetStaticPathsgetStaticPaths: type GetStaticPaths<Params extends ParsedUrlQuery = ParsedUrlQuery> = (context: GetStaticPathsContext) => Promise<GetStaticPathsResult<Params>> | GetStaticPathsResult<Params>GetStaticPaths = async () => {
const const posts: {
id: string;
}[]
posts = await const prisma: {
post: {
findMany: (opts: {
select: {
id: true;
};
}) => Promise<{
id: string;
}[]>;
};
}
prisma.post: {
findMany: (opts: {
select: {
id: true;
};
}) => Promise<{
id: string;
}[]>;
}
post.findMany: (opts: {
select: {
id: true;
};
}) => Promise<{
id: string;
}[]>
findMany({
select: {
id: true;
}
select: {
id: trueid: true,
},
});
return {
paths: (string | {
params: ParsedUrlQuery;
locale?: string;
})[]
paths: const posts: {
id: string;
}[]
posts.Array<{ id: string; }>.map<{
params: {
id: string;
};
}>(callbackfn: (value: {
id: string;
}, index: number, array: {
id: string;
}[]) => {
params: {
id: string;
};
}, thisArg?: any): {
params: {
id: string;
};
}[]
map((post: {
id: string;
}
post) => ({
params: {
id: string;
}
params: {
id: stringid: post: {
id: string;
}
post.id: stringid,
},
})),
// https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-blocking
fallback: GetStaticPathsFallbackfallback: 'blocking',
};
};
export default function function PostViewPage(props: InferGetStaticPropsType<typeof getStaticProps>): JSX.ElementPostViewPage(
props: {
trpcState: DehydratedState;
id: string;
}
props: type InferGetStaticPropsType<T extends (args: any) => any> = Extract<Awaited<ReturnType<T>>, {
props: any;
}>["props"]
InferGetStaticPropsType<typeof function getStaticProps(context: GetStaticPropsContext<{
id: string;
}>): Promise<{
props: {
trpcState: DehydratedState;
id: string;
};
revalidate: number;
}>
getStaticProps>,
) {
const { const id: stringid } = props: {
trpcState: DehydratedState;
id: string;
}
props;
const const postQuery: UseTRPCQueryResult<{
id: string;
title: string;
text: string;
createdAt: Date;
}, TRPCClientErrorLike<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
transformer: true;
errorShape: DefaultErrorShape;
}>>
postQuery = const trpc: CreateTRPCReactBase<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
post: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>;
}>>, unknown> & DecorateRouterRecord<...>
trpc.post: DecorateRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: true;
}, DecorateCreateRouterOptions<{
byId: QueryProcedure<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
post.byId: DecoratedQuery<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
transformer: true;
errorShape: DefaultErrorShape;
}>
byId.useQuery: ProcedureUseQuery
<{
id: string;
title: string;
text: string;
createdAt: Date;
}, {
id: string;
title: string;
text: string;
createdAt: Date;
}>(input: {
id: string;
} | unique symbol, opts?: UseTRPCQueryOptions<{
id: string;
title: string;
text: string;
createdAt: Date;
}, {
id: string;
title: string;
text: string;
createdAt: Date;
}, TRPCClientErrorLike<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
transformer: true;
errorShape: DefaultErrorShape;
}>, {
id: string;
title: string;
text: string;
createdAt: Date;
}> | undefined) => UseTRPCQueryResult<...> (+1 overload)
useQuery({ id: stringid });
if (const postQuery: UseTRPCQueryResult<{
id: string;
title: string;
text: string;
createdAt: Date;
}, TRPCClientErrorLike<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
transformer: true;
errorShape: DefaultErrorShape;
}>>
postQuery.status: "error" | "success" | "pending"status !== 'success') {
// won't happen since we're using `fallback: "blocking"`
return <>Loading...</>;
}
const { const data: {
id: string;
title: string;
text: string;
createdAt: Date;
}
data } = const postQuery: (TRPCHookResult & QueryObserverSuccessResult<{
id: string;
title: string;
text: string;
createdAt: Date;
}, TRPCClientErrorLike<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
transformer: true;
errorShape: DefaultErrorShape;
}>>) | (TRPCHookResult & QueryObserverPlaceholderResult<{
id: string;
title: string;
text: string;
createdAt: Date;
}, TRPCClientErrorLike<{
input: {
id: string;
};
output: {
id: string;
title: string;
text: string;
createdAt: Date;
};
transformer: true;
errorShape: DefaultErrorShape;
}>>)
postQuery;
return (
<>
<JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const data: {
id: string;
title: string;
text: string;
createdAt: Date;
}
data.title: stringtitle}</JSX.IntrinsicElements.h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>
<JSX.IntrinsicElements.em: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>em>Created {const data: {
id: string;
title: string;
text: string;
createdAt: Date;
}
data.createdAt: DatecreatedAt.Date.toLocaleDateString(locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions): string (+2 overloads)toLocaleDateString('en-us')}</JSX.IntrinsicElements.em: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>em>
<JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const data: {
id: string;
title: string;
text: string;
createdAt: Date;
}
data.text: stringtext}</JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
<JSX.IntrinsicElements.h2: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h2>Raw data:</JSX.IntrinsicElements.h2: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h2>
<JSX.IntrinsicElements.pre: DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement>pre>{var JSON: JSONJSON.JSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)stringify(const data: {
id: string;
title: string;
text: string;
createdAt: Date;
}
data, null, 4)}</JSX.IntrinsicElements.pre: DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement>pre>
</>
);
}Note that the default behaviour of react-query is to refetch the data on the client-side when it mounts, so if you want to only fetch the data via getStaticProps, you need to set refetchOnMount and refetchOnWindowFocus to false in the query options.
This might be preferable if you want to minimize the number of requests to your API, which might be necessary if you're using a third-party rate-limited API for example.
This can be done per query:
import { const trpc: CreateTRPCReactBase<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
example: QueryProcedure<{
input: void;
output: string;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>, unknown> & DecorateRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
example: QueryProcedure<{
input: void;
output: string;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
trpc } from './utils/trpc';
const const data: UseTRPCQueryResult<string, TRPCClientErrorLike<{
input: void;
output: string;
transformer: false;
errorShape: DefaultErrorShape;
}>>
data = const trpc: CreateTRPCReactBase<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
example: QueryProcedure<{
input: void;
output: string;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>, unknown> & DecorateRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
example: QueryProcedure<{
input: void;
output: string;
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
trpc.example: DecoratedQuery<{
input: void;
output: string;
transformer: false;
errorShape: DefaultErrorShape;
}>
example.useQuery: ProcedureUseQuery
<string, string>(input: void | unique symbol, opts?: UseTRPCQueryOptions<string, string, TRPCClientErrorLike<{
input: void;
output: string;
transformer: false;
errorShape: DefaultErrorShape;
}>, string> | undefined) => UseTRPCQueryResult<string, TRPCClientErrorLike<{
input: void;
output: string;
transformer: false;
errorShape: DefaultErrorShape;
}>> (+1 overload)
useQuery(
// if your query takes no input, make sure that you don't
// accidentally pass the query options as the first argument
var undefinedundefined,
{ refetchOnMount?: boolean | "always" | ((query: Query<string, TRPCClientErrorLike<{
input: void;
output: string;
transformer: false;
errorShape: DefaultErrorShape;
}>, string, any>) => boolean | "always") | undefined
refetchOnMount: false, refetchOnWindowFocus?: boolean | "always" | ((query: Query<string, TRPCClientErrorLike<{
input: void;
output: string;
transformer: false;
errorShape: DefaultErrorShape;
}>, string, any>) => boolean | "always") | undefined
refetchOnWindowFocus: false },
);Or globally, if every query across your app should behave the same way:
import { function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink } from '@trpc/client';
import { function createTRPCNext<TRouter extends AnyRouter, TSSRContext extends NextPageContext = NextPageContext>(opts: WithTRPCNoSSROptions<TRouter> | WithTRPCSSROptions<TRouter>): CreateTRPCNext<TRouter, TSSRContext>createTRPCNext } from '@trpc/next';
import class superjsonsuperjson from 'superjson';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from './api/trpc/[trpc]';
export const const trpc: CreateTRPCNextBase<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, NextPageContext> & DecorateRouterRecord<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
trpc = createTRPCNext<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, NextPageContext>(opts: WithTRPCNoSSROptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | WithTRPCSSROptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>): CreateTRPCNextBase<...> & DecorateRouterRecord<...>
createTRPCNext<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
config: (info: {
ctx?: NextPageContext;
}) => WithTRPCConfig<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
config(config: {
ctx?: NextPageContext;
}
config) {
return {
links: TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links: [
httpBatchLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: HTTPBatchLinkOptions<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>): TRPCLink<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
httpBatchLink({
url: string | URLurl: `${function getBaseUrl(): stringgetBaseUrl()}/api/trpc`,
}),
],
// Change options globally
queryClientConfig?: QueryClientConfig | undefinedqueryClientConfig: {
QueryClientConfig.defaultOptions?: DefaultOptions<Error> | undefineddefaultOptions: {
DefaultOptions<Error>.queries?: OmitKeyof<QueryObserverOptions<unknown, Error, unknown, unknown, readonly unknown[], never>, "suspense" | "queryKey", "strictly"> | undefinedqueries: {
refetchOnMount?: boolean | "always" | ((query: Query<unknown, Error, unknown, readonly unknown[]>) => boolean | "always") | undefinedrefetchOnMount: false,
refetchOnWindowFocus?: boolean | "always" | ((query: Query<unknown, Error, unknown, readonly unknown[]>) => boolean | "always") | undefinedrefetchOnWindowFocus: false,
},
},
},
};
},
});Be careful with this approach if your app has a mixture of static and dynamic queries.