Set up with Next.js Pages Router
On this page
The Pages Router uses the same client as the App Router: the TanStack React Query integration (@trpc/tanstack-react-query). You mount its provider in _app, and prefetch a page's queries in getServerSideProps or getStaticProps when you want them in the first HTML.
Recommended file structure
We recommend a file structure like this one, although it is not enforced by tRPC. The rest of this page will take you through the process of adding tRPC in to this structure.
.
├── prisma # <-- if prisma is added
│ └── [..]
├── src
│ ├── pages
│ │ ├── _app.tsx # <-- mount the providers here
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc].ts # <-- tRPC HTTP handler
│ │ └── [..]
│ ├── server
│ │ ├── routers
│ │ │ ├── _app.ts # <-- main app router
│ │ │ ├── post.ts # <-- sub routers
│ │ │ └── [..]
│ │ ├── context.ts # <-- create app context
│ │ └── trpc.ts # <-- procedure helpers
│ └── utils
│ ├── query-client.ts # <-- shared QueryClient factory
│ └── trpc.ts # <-- your typesafe tRPC hooks
└── [..]Add tRPC to existing Next.js project
1. Install deps
pnpm add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zodnpm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zodyarn add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zodbun add @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query@latest zod2. Enable strict mode
If you want to use Zod for input validation, make sure you have enabled strict mode in your tsconfig.json:
"compilerOptions": {
+ "strict": true
}If strict mode is too harsh, you'll at least want to enable strictNullChecks:
"compilerOptions": {
+ "strictNullChecks": true
}3. Create a tRPC router
Initialize your tRPC backend in src/server/trpc.ts using the initTRPC function, and create your first router. We're going to make a simple "hello world" router and procedure here - but for deeper information on creating your tRPC API you should refer to:
- the Quickstart guide and Backend usage docs for tRPC information
- the Next.js Adapter docs for mounting tRPC within your Next.js server.
View sample backend
import { const initTRPC: TRPCBuilder<object, object>initTRPC } from '@trpc/server';
// 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 const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t = const initTRPC: TRPCBuilder<object, object>initTRPC.TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
create();
// Base router and procedure helpers
export const const router: RouterBuilder<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
router = const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
router;
export const const procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure = const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
t.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;import { import zz } from 'zod';
import { const procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure, const router: RouterBuilder<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}>
router } from '../trpc';
export const const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
appRouter = router<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>(_: {
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}): BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
router({
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>
hello: const procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure
.ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodObject<{
text: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
text: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
text: string;
}, {
text: string;
}, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input(
import zz.function object<{
text: z.ZodString;
}>(shape?: {
text: z.ZodString;
} | undefined, params?: string | {
error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
text: z.ZodString;
}, z.core.$strip>
object({
text: z.ZodStringtext: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string(),
}),
)
.ProcedureBuilder<object, object, object, { text: string; }, { text: string; }, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.query<{
greeting: string;
}>(resolver: ProcedureResolver<object, object, object, {
text: string;
}, UnsetMarker, {
greeting: string;
}>): QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>
query((opts: ProcedureResolverOptions<object, object, object, {
text: string;
}>
opts) => {
return {
greeting: stringgreeting: `hello ${opts: ProcedureResolverOptions<object, object, object, {
text: string;
}>
opts.ProcedureResolverOptions<object, object, object, { text: string; }>.input: {
text: string;
}
input.text: stringtext}`,
};
}),
});
// export type definition of API
export type type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>> & DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>
AppRouter = typeof const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>
appRouter;import * as import trpcNexttrpcNext from '@trpc/server/adapters/next';
import { const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter } from '../../../server/routers/_app';
// export API handler
// @link https://trpc.io/docs/server/adapters
export default import trpcNexttrpcNext.function createNextApiHandler<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: NodeHTTPHandlerOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, trpcNext.NextApiRequest, trpcNext.NextApiResponse>): trpcNext.NextApiHandler
createNextApiHandler({
BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, NextApiRequest>.router: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
router: const appRouter: BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter,
createContext?: NodeHTTPCreateContextFn<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, trpcNext.NextApiRequest, trpcNext.NextApiResponse> | undefined
createContext: () => ({}),
});4. Create a Query Client factory
Create utils/query-client.ts. Both _app and the pages that prefetch on the server use it.
import { class QueryClientQueryClient } from '@tanstack/react-query';
export function function makeQueryClient(): QueryClientmakeQueryClient() {
return new new QueryClient(config?: QueryClientConfig): QueryClientQueryClient({
QueryClientConfig.defaultOptions?: DefaultOptions<Error> | undefineddefaultOptions: {
DefaultOptions<Error>.queries?: OmitKeyof<QueryObserverOptions<unknown, Error, unknown, unknown, readonly unknown[], never>, "suspense" | "queryKey", "strictly"> | undefinedqueries: {
// keep server-rendered data fresh for a while, so the
// client doesn't fetch it again as soon as the page loads
staleTime?: StaleTimeFunction<unknown, Error, unknown, readonly unknown[]> | undefinedstaleTime: 30 * 1000,
},
},
});
}5. Create tRPC hooks
Use createTRPCContext to create a provider and a useTRPC hook from your API's type signature.
import { function createTRPCContext<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
keyPrefix: false;
}>(): CreateTRPCContextResult<TRouter, TFeatureFlags>
createTRPCContext } from '@trpc/tanstack-react-query';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from '../server/routers/_app';
export const { const TRPCProvider: TRPCProviderType<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>
TRPCProvider, const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>
useTRPC } = createTRPCContext<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>(): CreateTRPCContextResult<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>
createTRPCContext<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>();6. Configure _app.tsx
Create the query client and the tRPC client once per app, and wrap every page in the providers. HydrationBoundary puts the queries a page prefetched on the server, which it returns as trpcState, into the cache.
import type { DehydratedState } from '@tanstack/react-query';
import { const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary, const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider } from '@tanstack/react-query';
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function httpBatchLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchLink } from '@trpc/client';
import type { type AppProps<P = any> = AppInitialProps<P> & {
Component: NextComponentType<NextPageContext, any, any>;
router: Router;
__N_SSG?: boolean;
__N_SSP?: boolean;
}
AppProps } from 'next/app';
import { function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] (+1 overload)useState } from 'react';
import type { type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter } from '../server/routers/_app';
import { function makeQueryClient(): QueryClientmakeQueryClient } from '../utils/query-client';
import { const TRPCProvider: TRPCProviderType<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>
TRPCProvider } from '../utils/trpc';
export default function function App({ Component, pageProps, }: AppProps<{
trpcState?: DehydratedState;
}>): JSX.Element
App({
type Component: NextComponentType<NextPageContext, any, any>Component,
pageProps: {
trpcState?: DehydratedState;
}
pageProps,
}: type AppProps<P = any> = AppInitialProps<P> & {
Component: NextComponentType<NextPageContext, any, any>;
router: Router;
__N_SSG?: boolean;
__N_SSP?: boolean;
}
AppProps<{ trpcState?: DehydratedState | undefinedtrpcState?: DehydratedState }>) {
const [const queryClient: QueryClientqueryClient] = useState<QueryClient>(initialState: QueryClient | (() => QueryClient)): [QueryClient, Dispatch<SetStateAction<QueryClient>>] (+1 overload)useState(function makeQueryClient(): QueryClientmakeQueryClient);
const [const trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpcClient] = useState<TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>>(initialState: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>> | (() => TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>)): [...] (+1 overload)
useState(() =>
createTRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient<type AppRouter = Router<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter>({
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({
// transformer: superjson, <-- if you use a data transformer
url: string | URLurl: '/api/trpc',
}),
],
}),
);
return (
<const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider client: QueryClientclient={const queryClient: QueryClientqueryClient}>
<const TRPCProvider: TRPCProviderType<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>
TRPCProvider trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpcClient={const trpcClient: TRPCClient<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>>
trpcClient} queryClient: QueryClientqueryClient={const queryClient: QueryClientqueryClient}>
<const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary HydrationBoundaryProps.state: DehydratedState | null | undefinedstate={pageProps: {
trpcState?: DehydratedState;
}
pageProps.trpcState?: DehydratedState | undefinedtrpcState}>
<type Component: NextComponentType<NextPageContext, any, any>Component {...pageProps: {
trpcState?: DehydratedState;
}
pageProps} />
</const HydrationBoundary: ({ children, options, state, queryClient }: HydrationBoundaryProps) => React.ReactElementHydrationBoundary>
</const TRPCProvider: TRPCProviderType<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{}>>, {
keyPrefix: false;
}>
TRPCProvider>
</const QueryClientProvider: ({ client, children }: QueryClientProviderProps) => React.JSX.ElementQueryClientProvider>
);
}The client only calls the API from the browser, so a relative URL is enough. On the server, pages call your procedures directly, as Server-Side Rendering shows.
7. Make an API request
You're all set! Get the options for a query from useTRPC(), and pass them to TanStack React Query's hooks. For more, see TanStack React Query usage.
import { function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<TData, TError> (+2 overloads)useQuery } from '@tanstack/react-query';
import { const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>, {
keyPrefix: false;
}>
useTRPC } from '../utils/trpc';
export default function function IndexPage(): JSX.ElementIndexPage() {
const const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>, {
keyPrefix: false;
}>
trpc = function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>, {
keyPrefix: false;
}>
useTRPC();
const const helloQuery: UseQueryResult<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
helloQuery = useQuery<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
greeting: string;
}, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
greeting: string;
}, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<...> (+2 overloads)
useQuery(const trpc: TRPCOptionsProxy<BuiltRouter<{
ctx: object;
meta: object;
errorShape: DefaultErrorShape;
transformer: false;
}, DecorateCreateRouterOptions<{
hello: QueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
meta: object;
errorShape: DefaultErrorShape;
}>;
}>>, {
keyPrefix: false;
}>
trpc.hello: DecorateQueryProcedure<{
input: {
text: string;
};
output: {
greeting: string;
};
transformer: false;
errorShape: DefaultErrorShape;
featureFlags: {
keyPrefix: false;
};
}> & Record<string, never>
hello.DecorateQueryProcedure<{ input: { text: string; }; output: { greeting: string; }; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<{
greeting: string;
}, {
greeting: string;
}>(input: {
text: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<{
greeting: string;
}, {
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<{
greeting: string;
}, {
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>, {
keyPrefix: false;
}> (+2 overloads)
queryOptions({ text: stringtext: 'client' }));
switch (const helloQuery: UseQueryResult<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
helloQuery.status: "pending" | "error" | "success"status) {
case 'pending':
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading...</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
case 'error':
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Error: {const helloQuery: QueryObserverRefetchErrorResult<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>> | QueryObserverLoadingErrorResult<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
helloQuery.error: TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>
error.TRPCClientErrorBase<DefaultErrorShape>.message: stringmessage}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
case 'success':
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const helloQuery: QueryObserverSuccessResult<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>> | QueryObserverPlaceholderResult<{
greeting: string;
}, TRPCClientErrorLike<{
transformer: false;
errorShape: DefaultErrorShape;
}>>
helloQuery.data: {
greeting: string;
}
data.greeting: stringgreeting}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>;
}
}This page fetches in the browser. To put the data in the first HTML, prefetch it in getServerSideProps or getStaticProps.
Using a data transformer
Next.js sends page props as JSON, and checks that they are. So without a transformer, the queries you prefetch can only hold JSON values, in their inputs as well as their results: a Date or an undefined field makes getServerSideProps fail.
If your router uses a data transformer such as superjson, run the whole prefetched cache through it. In getServerSideProps or getStaticProps, return trpcState: superjson.serialize(dehydrate(queryClient)), as the SSG example does. Then read it back with a hook:
import type { DehydratedState } from '@tanstack/react-query';
import { function useMemo<T>(factory: () => T, deps: React.DependencyList): TuseMemo } from 'react';
import class superjsonsuperjson from 'superjson';
export type type TRPCState = SuperJSONResultTRPCState = type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof class superjsonsuperjson.SuperJSON.serialize: (object: SuperJSONValue) => SuperJSONResultserialize>;
export function function useTRPCState(trpcState: TRPCState | undefined): DehydratedState | undefineduseTRPCState(trpcState: SuperJSONResult | undefinedtrpcState: type TRPCState = SuperJSONResultTRPCState | undefined) {
return useMemo<DehydratedState | undefined>(factory: () => DehydratedState | undefined, deps: React.DependencyList): DehydratedState | undefineduseMemo(
() => trpcState: SuperJSONResult | undefinedtrpcState && class superjsonsuperjson.SuperJSON.deserialize: <DehydratedState>(payload: SuperJSONResult, options?: {
inPlace?: boolean;
}) => DehydratedState
deserialize<DehydratedState>(trpcState: SuperJSONResulttrpcState),
[trpcState: SuperJSONResult | undefinedtrpcState],
);
}In the _app from step 6, change three things:
- Type the props as
AppProps<{ trpcState?: TRPCState }>. - Pass
transformer: superjsontohttpBatchLink. - Call
const trpcState = useTRPCState(pageProps.trpcState), and passtrpcStatetoHydrationBoundaryinstead ofpageProps.trpcState.
Moving from createTRPCNext
createTRPCNext combined the classic React Query hooks with a higher-order component for _app. Here's what replaces each part:
@trpc/next | @trpc/tanstack-react-query |
|---|---|
createTRPCNext<AppRouter>({ config }) | createTRPCContext<AppRouter>(), plus a createTRPCClient with your links in _app |
trpc.withTRPC(MyApp) | QueryClientProvider, TRPCProvider and HydrationBoundary in _app, as in step 6 |
trpc.post.byId.useQuery(input) | useQuery(trpc.post.byId.queryOptions(input)), with trpc from useTRPC() |
trpc.useUtils() | useQueryClient() with trpc.post.byId.queryKey() or queryFilter() |
ssr: true and ssrPrepass | No direct replacement. Prefetch each page's queries in getServerSideProps |
responseMeta | Set the status and headers in getServerSideProps |
config({ ctx }) forwarding the request headers | Pass a context built from context.req to createTRPCOptionsProxy in getServerSideProps |
createServerSideHelpers (@trpc/react-query/server) | createTRPCOptionsProxy with a query client, then dehydrate(queryClient). See Server-Side Helpers |
abortOnUnmount in config | trpc: { abortOnUnmount: true } on each query. See Aborting Procedure Calls |
queryClientConfig | The options you pass to new QueryClient() in makeQueryClient |
The migration guide covers the hooks in detail, and has a codemod for them that's still a work in progress.
ssr: true rendered the page over and over until no query was fetching. With getServerSideProps, you pick which queries run on the server, and Next.js keeps Automatic Static Optimization for pages that don't need them.
Common mistakes
- Sharing one
QueryClientacross requests. Make it in_appwithuseState, and make a new one in eachgetServerSidePropsorgetStaticProps. A query client at module scope would hand one user's data to the next. - Returning the dehydrated state under another key.
_appreadspageProps.trpcState, so return{ props: { trpcState: dehydrate(queryClient) } }. Otherwise the client fetches every query again. - Prefetching with a different input than the page uses. The input is part of the query key, so the page misses the prefetched data and fetches again.
- Prefetching values JSON can't hold. Next.js sends page props as JSON, so a
Dateor anundefinedfield in a query's input or result makes the page fail. Run the prefetched cache through a data transformer. - Mixing in App Router setup.
HydrateClientandserver-onlyare for the App Router. In the Pages Router, prefetch ingetServerSidePropsorgetStaticPropsinstead.
Next steps
Browse the rest of the docs to learn more about things like authorization, middlewares, and error handling.
You can also find information about queries, mutations and invalidation in the TanStack React Query docs.
Pages under Pages Router
- Server-Side Rendering (SSR)Prefetch a page's tRPC queries in getServerSideProps with createTRPCOptionsProxy, build the context from the request, and set the status and cache headers
- Static Site Generation (SSG)Prefetch tRPC queries in getStaticProps with createTRPCOptionsProxy and dehydrate, and stop the client from refetching them
- Server-Side HelpersPrefetch or fetch tRPC queries in getServerSideProps and getStaticProps with createTRPCOptionsProxy and a query client, then pass them to the page as trpcState
- Aborting Procedure CallsCancel in-flight requests when a component unmounts with the abortOnUnmount option on a query