Skip to content
tRPC

Set up with TanStack Start

On this page

This guide shows how to use tRPC with TanStack Start: route loaders start queries, and components read them through handles.

tRPC adds no router integration of its own. Data crosses from the server to the browser through TanStack's SSR query integration, with your tRPC data transformer.

The SSE chat example is a complete TanStack Start app that uses handles in its loaders.

1. Install deps

npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query @tanstack/react-router-ssr-query superjson zod

2. Create a query client factory

Use your tRPC transformer to serialize query data, and set a staleTime above 0 so the browser doesn't refetch right after hydration:

src/lib/query-client.ts
import { class QueryClientQueryClient } from '@tanstack/react-query';
import class superjsonsuperjson from 'superjson';

export function function createQueryClient(): QueryClientcreateQueryClient() {
  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: { staleTime?: StaleTimeFunction<unknown, Error, unknown, readonly unknown[]> | undefinedstaleTime: 30 * 1000 },
      DefaultOptions<Error>.dehydrate?: DehydrateOptions | undefineddehydrate: { DehydrateOptions.serializeData?: TransformerFn | undefinedserializeData: class superjsonsuperjson.SuperJSON.serialize: (object: SuperJSONValue) => SuperJSONResultserialize },
      
DefaultOptions<Error>.hydrate?: {
    deserializeData?: TransformerFn;
    queries?: QueryOptions;
    mutations?: MutationOptions<unknown, DefaultError, unknown, unknown>;
} | undefined
hydrate
: { deserializeData?: TransformerFn | undefineddeserializeData: class superjsonsuperjson.
SuperJSON.deserialize: <T = unknown>(payload: SuperJSONResult, options?: {
    inPlace?: boolean;
}) => T
deserialize
},
}, }); }

3. Create an isomorphic tRPC client

During server rendering, call procedures in the same process with localLink. In the browser, call them over HTTP:

src/lib/trpc.ts
import { function createIsomorphicFn(): IsomorphicFnBasecreateIsomorphicFn } from '@tanstack/react-start';
import { function getRequest(): RequestgetRequest } from '@tanstack/react-start/server';
import {
  function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient,
  function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchStreamLinkOptions<TRouter["_def"]["_config"]["$types"]>): TRPCLink<TRouter>httpBatchStreamLink,
  function localLink<TRouter extends AnyRouter>(opts: LocalLinkOptions<TRouter>): TRPCLink<TRouter>localLink,
  type type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>TRPCLink,
} from '@trpc/client';
import {
  
function createTRPCContext<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
    keyPrefix: false;
}>(): CreateTRPCContextResult<TRouter, TFeatureFlags>
createTRPCContext
,
type type TRPCOptionsProxy<TRouter extends AnyTRPCRouter, TFeatureFlags extends FeatureFlags = { keyPrefix: false; }> = DecoratedRouterRecord<TRouter["_def"]["_config"]["$types"], TRouter["_def"]["record"], TFeatureFlags> & DecorateRouterKeyable<TFeatureFlags> & DecorateHandleReaders<TFeatureFlags>TRPCOptionsProxy, } from '@trpc/tanstack-react-query'; import class superjsonsuperjson from 'superjson'; import {
function createContext(opts: {
    req: Request;
}): Promise<{
    req: Request;
}>
createContext
} from '~/server/context';
import {
const appRouter: BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>
appRouter
, type
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
} from '~/server/routers/_app';
export const {
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
TRPCProvider
,
const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
useTRPC
} =
createTRPCContext<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>(): CreateTRPCContextResult<...>
createTRPCContext
<
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>();
export type
type TRPC = DecoratedRouterRecord<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>, {
    ...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
TRPC
= type TRPCOptionsProxy<TRouter extends AnyTRPCRouter, TFeatureFlags extends FeatureFlags = { keyPrefix: false; }> = DecoratedRouterRecord<TRouter["_def"]["_config"]["$types"], TRouter["_def"]["record"], TFeatureFlags> & DecorateRouterKeyable<TFeatureFlags> & DecorateHandleReaders<TFeatureFlags>TRPCOptionsProxy<
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>;
const
const getLinks: IsomorphicFn<[], TRPCLink<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>[], TRPCLink<...>[]>
getLinks
= function createIsomorphicFn(): IsomorphicFnBasecreateIsomorphicFn()
.
IsomorphicFnBase.server: <[], TRPCLink<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>[]>(serverImpl: () => TRPCLink<...>[]) => ServerOnlyFn<...>
server
((): type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>TRPCLink<
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>[] => [
localLink<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>(opts: LocalLinkOptions<...>): TRPCLink<...>
localLink
({
router: BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>
router
:
const appRouter: BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>
appRouter
,
createContext: () => Promise<{
    req: Request;
}>
createContext
: () =>
function createContext(opts: {
    req: Request;
}): Promise<{
    req: Request;
}>
createContext
({ req: Requestreq: function getRequest(): RequestgetRequest() }),
transformer: DataTransformerOptionstransformer: class superjsonsuperjson, }), ]) .
ServerOnlyFn<[], TRPCLink<BuiltRouter<{ ctx: { req: Request; }; meta: object; errorShape: DefaultErrorShape; transformer: true; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: { req: Request; }; meta: object; errorShape: DefaultErrorShape; transformer: true; }, DecorateCreateRouterOptions<...>>; }>>>[]>.client: <TRPCLink<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>[]>(clientImpl: () => TRPCLink<...>[]) => IsomorphicFn<...>
client
((): type TRPCLink<TInferrable extends InferrableClientTypes> = (opts: TRPCClientRuntime) => OperationLink<TInferrable>TRPCLink<
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>[] => [
httpBatchStreamLink<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>(opts: HTTPBatchStreamLinkOptions<...>): TRPCLink<...>
httpBatchStreamLink
({ url: string | URLurl: '/api/trpc', transformer: DataTransformerOptionstransformer: class superjsonsuperjson }),
]); export const
const makeTRPCClient: () => TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
makeTRPCClient
= () =>
createTRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>(opts: CreateTRPCClientOptions<...>): TRPCClient<...>
createTRPCClient
<
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>[]
links
:
const getLinks: () => TRPCLink<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>[]
getLinks
() });

The browser's link calls /api/trpc. Serve it from a server route with the fetch adapter:

src/routes/api/trpc.$.ts
import { function createFileRoute<TFilePath extends keyof FileRoutesByPath, TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]["parentRoute"], TId extends RouteConstraints["TId"] = FileRoutesByPath[TFilePath]["id"], TPath extends RouteConstraints["TPath"] = FileRoutesByPath[TFilePath]["path"], TFullPath extends RouteConstraints["TFullPath"] = FileRoutesByPath[TFilePath]["fullPath"]>(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>["createRoute"]createFileRoute } from '@tanstack/react-router';
import { function fetchRequestHandler<TRouter extends AnyRouter>(opts: FetchHandlerRequestOptions<TRouter>): Promise<Response>fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { 
function createContext(opts: {
    req: Request;
}): Promise<{
    req: Request;
}>
createContext
} from '~/server/context';
import {
const appRouter: BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>
appRouter
} from '~/server/routers/_app';
const
const handler: ({ request }: {
    request: Request;
}) => Promise<Response>
handler
= ({ request: Requestrequest }: { request: Requestrequest: Request }) =>
fetchRequestHandler<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>(opts: FetchHandlerRequestOptions<...>): Promise<Response>
fetchRequestHandler
({
endpoint: stringendpoint: '/api/trpc', req: Requestreq: request: Requestrequest,
BaseHandlerOptions<BuiltRouter<{ ctx: { req: Request; }; meta: object; errorShape: DefaultErrorShape; transformer: true; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: { req: Request; }; meta: object; errorShape: DefaultErrorShape; transformer: true; }, DecorateCreateRouterOptions<...>>; }>>, Request>.router: BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>
router
:
const appRouter: BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>
appRouter
,
createContext: FetchCreateContextFn<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
createContext
,
}); export const
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/api/trpc/$", "/api/trpc/$", "/api/trpc/$", "/api/trpc/$", undefined, ResolveParams<"/api/trpc/$">, AnyContext, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, {
    GET: ({ request }: {
        request: Request;
    }) => Promise<Response>;
    POST: ({ request }: {
        request: Request;
    }) => Promise<Response>;
}>
Route
=
createFileRoute<"/api/trpc/$", RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/api/trpc/$", "/api/trpc/$", "/api/trpc/$">(path?: "/api/trpc/$" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
('/api/trpc/$')({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., { ...; }>.server?: RouteServerOptions<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/api/trpc/$", ResolveParams<"/api/trpc/$">, {}, undefined, AnyContext, AnyContext, AnyContext, unknown, {
    GET: ({ request }: {
        request: Request;
    }) => Promise<Response>;
    POST: ({ request }: {
        request: Request;
    }) => Promise<Response>;
}> | undefined
server
: {
RouteServerOptions<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 8 more ..., { ...; }>.handlers?: Constrain<{
    GET: ({ request }: {
        request: Request;
    }) => Promise<Response>;
    POST: ({ request }: {
        request: Request;
    }) => Promise<Response>;
}, Partial<...> | ((opts: HandlersFnOpts<...>) => CustomHandlerFunctionsRecord<...>)> | undefined
handlers
: {
type GET: ({ request }: {
    request: Request;
}) => Promise<Response>
GET
:
const handler: ({ request }: {
    request: Request;
}) => Promise<Response>
handler
,
type POST: ({ request }: {
    request: Request;
}) => Promise<Response>
POST
:
const handler: ({ request }: {
    request: Request;
}) => Promise<Response>
handler
} },
});

4. Put the proxy in the router context

Create one proxy per router with createTRPCOptionsProxy, and pass it to loaders through the router context:

src/router.tsx
import { const createRouter: CreateRouterFncreateRouter } from '@tanstack/react-router';
import { function setupRouterSsrQueryIntegration<TRouter extends AnyRouter>(opts: Options<TRouter>): voidsetupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query';
import { 
function createTRPCOptionsProxy<TRouter extends AnyRouter, TFeatureFlags extends FeatureFlags = {
    keyPrefix: false;
}>(opts: TRPCOptionsProxyOptions<TRouter, TFeatureFlags>): TRPCOptionsProxy<TRouter, TFeatureFlags>
createTRPCOptionsProxy
} from '@trpc/tanstack-react-query';
import { function createQueryClient(): QueryClientcreateQueryClient } from '~/lib/query-client'; import {
const makeTRPCClient: () => TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
makeTRPCClient
,
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
TRPCProvider
} from '~/lib/trpc';
import type {
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
} from '~/server/routers/_app';
import {
const routeTree: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined>
routeTree
} from './routeTree.gen';
export function
function getRouter(): RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
getRouter
() {
const const queryClient: QueryClientqueryClient = function createQueryClient(): QueryClientcreateQueryClient(); const
const trpcClient: TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
trpcClient
=
function makeTRPCClient(): TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
makeTRPCClient
();
const
const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
trpc
=
createTRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>(opts: TRPCOptionsProxyOptions<...>): TRPCOptionsProxy<...>
createTRPCOptionsProxy
<
type AppRouter = Router<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>> & DecorateCreateRouterOptions<...>
AppRouter
>({
TRPCOptionsProxyOptionsExternal<BuiltRouter<{ ctx: { req: Request; }; meta: object; errorShape: DefaultErrorShape; transformer: true; }, DecorateCreateRouterOptions<{ post: BuiltRouter<{ ctx: { req: Request; }; meta: object; errorShape: DefaultErrorShape; transformer: true; }, DecorateCreateRouterOptions<...>>; }>>>.client: TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>> | TRPCUntypedClient<...>
client
:
const trpcClient: TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
trpcClient
,
queryClient: QueryClient | (() => QueryClient)queryClient, }); const
const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router
=
createRouter<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>(options: RouterConstructorOptions<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, ... 5 more ..., undefined>, "never", false, RouterHistory, Record<...>>): RouterCore<...>
createRouter
({
routeTree?: Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined> | undefined
routeTree
,
context: {
    queryClient: QueryClient;
    trpc: TRPC;
}
context
: { queryClient: QueryClientqueryClient, trpc: TRPCtrpc },
defaultPreload?: false | "intent" | "viewport" | "render" | undefineddefaultPreload: 'intent', // Let the query cache decide what's fresh: loaders always run, and fresh data is reused defaultPreloadStaleTime?: number | undefineddefaultPreloadStaleTime: 0,
Wrap?: ((props: {
    children: any;
}) => React.JSX.Element) | undefined
Wrap
: (
props: {
    children: any;
}
props
) => (
<
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
TRPCProvider
trpcClient: TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
trpcClient
={
const trpcClient: TRPCClient<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>>
trpcClient
} queryClient: QueryClientqueryClient={const queryClient: QueryClientqueryClient}>
{
props: {
    children: any;
}
props
.children: anychildren}
</
const TRPCProvider: TRPCProviderType<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
TRPCProvider
>
), });
setupRouterSsrQueryIntegration<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>>(opts: Options<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, ... 7 more ..., undefined>, "never", false, RouterHistory, Record<...>>>): void
setupRouterSsrQueryIntegration
({
router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router
, queryClient: QueryClientqueryClient });
return
const router: RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>
router
;
}

Type the root route's context with createRootRouteWithContext<{ queryClient: QueryClient; trpc: TRPC }>().

5. Start queries in loaders

A loader starts queries and returns their handles as loader data. For each query, choose whether the loader waits for it. Starting a query compares the two. Every input should come from the route's params or search params: see Start every query from the URL.

  • On the first visit, the loader runs on the server, and the data is sent with the page.
  • On later navigations, the loader runs in the browser against the tab's cache. Fresh data is reused; stale data renders at once and refetches.

Without waiting: unstable_$prefetch

The loader returns at once, and the route renders before the data arrives:

src/routes/posts.$id.comments.tsx
import { function createFileRoute<TFilePath extends keyof FileRoutesByPath, TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]["parentRoute"], TId extends RouteConstraints["TId"] = FileRoutesByPath[TFilePath]["id"], TPath extends RouteConstraints["TPath"] = FileRoutesByPath[TFilePath]["path"], TFullPath extends RouteConstraints["TFullPath"] = FileRoutesByPath[TFilePath]["fullPath"]>(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>["createRoute"]createFileRoute } from '@tanstack/react-router';
import { const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense } from 'react';
import { 
function Comments(props: {
    comments: unstable_inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments
} from '~/components/comments';
export const
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id/comments", "/posts/$id/comments", "/posts/$id/comments", "/posts/$id/comments", undefined, ResolveParams<"/posts/$id/comments">, AnyContext, AnyContext, AnyContext, {}, ({ context: { trpc }, params }: LoaderFnContext<Register, RootRoute<Register, ... 10 more ..., undefined>, ... 7 more ..., undefined>) => {
    ...;
}, ... 4 more ..., undefined>
Route
=
createFileRoute<"/posts/$id/comments", RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id/comments", "/posts/$id/comments", "/posts/$id/comments">(path?: "/posts/$id/comments" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
('/posts/$id/comments')({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context: { trpc }, params }: LoaderFnContext<Register, RootRoute<Register, ... 10 more ..., undefined>, ... 7 more ..., undefined>) => {
    ...;
}, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: ({
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient;
    trpc: TRPC;
}
context
: { trpc: TRPCtrpc },
params: {
    id: string;
}
params
}) => ({
comments: unstable_TRPCInfiniteQueryHandle<{
    input: {
        postId: string;
        limit?: number | undefined;
        cursor?: number | null | undefined;
    };
    output: {
        items: {
            id: string;
            text: string;
        }[];
        nextCursor: number | null;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
comments
: trpc: TRPCtrpc.
post: DecoratedRouterRecord<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: {
            id: string;
            title: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    comments: QueryProcedure<{
        input: {
            postId: string;
            limit?: number | undefined;
            cursor?: number | null | undefined;
        };
        output: {
            items: {
                id: string;
                text: string;
            }[];
            nextCursor: number | null;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    list: QueryProcedure<...>;
}>, {
    ...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
post
.
comments: DecorateQueryProcedure<{
    input: {
        postId: string;
        limit?: number | undefined;
        cursor?: number | null | undefined;
    };
    output: {
        items: {
            id: string;
            text: string;
        }[];
        nextCursor: number | null;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> & DecorateInfiniteQueryProcedure<{
    input: {
        postId: string;
        limit?: number | undefined;
        cursor?: number | null | undefined;
    };
    output: {
        items: {
            id: string;
            text: string;
        }[];
        nextCursor: number | null;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
comments
.
DecorateInfiniteQueryHandleVerbs<{ input: { postId: string; limit?: number | undefined; cursor?: number | null | undefined; }; output: { items: { id: string; text: string; }[]; nextCursor: number | null; }; transformer: true; errorShape: DefaultErrorShape; featureFlags: { ...; }; }>.unstable_$prefetchInfinite: (input: {
    postId: string;
    limit?: number | undefined;
    cursor?: number | null | undefined;
}, opts?: unstable_TRPCPrefetchInfiniteOptions<{
    input: {
        postId: string;
        limit?: number | undefined;
        cursor?: number | null | undefined;
    };
    output: {
        items: {
            id: string;
            text: string;
        }[];
        nextCursor: number | null;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> | undefined) => unstable_TRPCInfiniteQueryHandle<{
    input: {
        postId: string;
        limit?: number | undefined;
        cursor?: number | null | undefined;
    };
    output: {
        items: {
            id: string;
            text: string;
        }[];
        nextCursor: number | null;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
unstable_$prefetchInfinite
(
{ postId: stringpostId:
params: {
    id: string;
}
params
.id: stringid, limit?: number | undefinedlimit: 20 },
{
getNextPageParam: (last: {
    items: {
        id: string;
        text: string;
    }[];
    nextCursor: number | null;
}) => number | null
getNextPageParam
: (
last: {
    items: {
        id: string;
        text: string;
    }[];
    nextCursor: number | null;
}
last
) =>
last: {
    items: {
        id: string;
        text: string;
    }[];
    nextCursor: number | null;
}
last
.nextCursor: number | nullnextCursor },
), }), UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function CommentsPage(): JSX.ElementCommentsPage, }); function function CommentsPage(): JSX.ElementCommentsPage() { const
const loaderData: {
    comments: unstable_TRPCInfiniteQueryHandle<{
        input: {
            postId: string;
            limit?: number | undefined;
            cursor?: number | null | undefined;
        };
        output: {
            items: {
                id: string;
                text: string;
            }[];
            nextCursor: number | null;
        };
        transformer: true;
        errorShape: DefaultErrorShape;
        featureFlags: {
            keyPrefix: false;
        };
    }>;
}
loaderData
=
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id/comments", "/posts/$id/comments", "/posts/$id/comments", "/posts/$id/comments", undefined, ResolveParams<"/posts/$id/comments">, AnyContext, AnyContext, AnyContext, {}, ({ context: { trpc }, params }: LoaderFnContext<Register, RootRoute<Register, ... 10 more ..., undefined>, ... 7 more ..., undefined>) => {
    ...;
}, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/posts/$id/comments", "/posts/$id/comments">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {
    readonly PostsIdCommentsRoute: Route<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
        trpc: TRPC;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id/comments", "/posts/$id/comments", ... 13 more ..., undefined>;
}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
return ( <const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading comments…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function Comments(props: {
    comments: unstable_inferInfiniteQueryHandle<TRPC["post"]["comments"]>;
}): JSX.Element
Comments
comments: unstable_TRPCInfiniteQueryHandle<{
    input: {
        postId: string;
        limit?: number | undefined;
        cursor?: number | null | undefined;
    };
    output: {
        items: {
            id: string;
            text: string;
        }[];
        nextCursor: number | null;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
comments
={
const loaderData: {
    comments: unstable_TRPCInfiniteQueryHandle<{
        input: {
            postId: string;
            limit?: number | undefined;
            cursor?: number | null | undefined;
        };
        output: {
            items: {
                id: string;
                text: string;
            }[];
            nextCursor: number | null;
        };
        transformer: true;
        errorShape: DefaultErrorShape;
        featureFlags: {
            keyPrefix: false;
        };
    }>;
}
loaderData
.
comments: unstable_TRPCInfiniteQueryHandle<{
    input: {
        postId: string;
        limit?: number | undefined;
        cursor?: number | null | undefined;
    };
    output: {
        items: {
            id: string;
            text: string;
        }[];
        nextCursor: number | null;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
comments
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> ); }
  • Wrap the reader in Suspense. Comments suspends until the data arrives. Without your own boundary, the suspension reaches the nearest boundary above it: the route's pendingComponent, or a parent route's. That can blank much more of the page than you want.
  • The loader never throws for these queries. If a request fails, Comments throws, and the route's errorComponent (or the router's default) shows the error.
  • Pass getNextPageParam to infinite queries. The loader runs again on client navigations, and if the cached pages are stale, it refetches every page the user loaded. That needs getNextPageParam from page two on.

Waiting: unstable_$ensure

The loader waits, so the route renders with the data in place:

src/routes/posts.$id.tsx
import { function createFileRoute<TFilePath extends keyof FileRoutesByPath, TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]["parentRoute"], TId extends RouteConstraints["TId"] = FileRoutesByPath[TFilePath]["id"], TPath extends RouteConstraints["TPath"] = FileRoutesByPath[TFilePath]["path"], TFullPath extends RouteConstraints["TFullPath"] = FileRoutesByPath[TFilePath]["fullPath"]>(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>["createRoute"]createFileRoute, function notFound(options?: NotFoundError): NotFoundErrornotFound } from '@tanstack/react-router';
import { function isTRPCClientError<TInferrable extends InferrableClientTypes>(cause: unknown): cause is TRPCClientError<TInferrable>isTRPCClientError } from '@trpc/client';
import { 
function Post(props: {
    post: unstable_inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post
} from '~/components/post';
export const
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id", "/posts/$id", "/posts/$id", "/posts/$id", undefined, ResolveParams<"/posts/$id">, AnyContext, AnyContext, AnyContext, {}, ({ context: { trpc }, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/posts/$id", RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id", "/posts/$id", "/posts/$id">(path?: "/posts/$id" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
('/posts/$id')({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context: { trpc }, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async ({
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient;
    trpc: TRPC;
}
context
: { trpc: TRPCtrpc },
params: {
    id: string;
}
params
}) => {
const
const post: unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
post
= await trpc: TRPCtrpc.
post: DecoratedRouterRecord<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: {
            id: string;
            title: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    comments: QueryProcedure<{
        input: {
            postId: string;
            limit?: number | undefined;
            cursor?: number | null | undefined;
        };
        output: {
            items: {
                id: string;
                text: string;
            }[];
            nextCursor: number | null;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    list: QueryProcedure<...>;
}>, {
    ...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
post
.
byId: DecorateQueryProcedure<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> & Record<string, never>
byId
.
DecorateQueryHandleVerbs<{ input: { id: string; }; output: { id: string; title: string; }; transformer: true; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.unstable_$ensure: (input: {
    id: string;
}, opts?: unstable_TRPCPrefetchOptions | undefined) => Promise<unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>>
unstable_$ensure
({ id: stringid:
params: {
    id: string;
}
params
.id: stringid })
.
Promise<unstable_TRPCQueryHandle<{ input: { id: string; }; output: { id: string; title: string; }; transformer: true; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>>.catch<never>(onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>>
catch
((cause: anycause) => {
if (isTRPCClientError<InferrableClientTypes>(cause: unknown): cause is TRPCClientError<InferrableClientTypes>isTRPCClientError(cause: anycause) && cause: TRPCClientError<InferrableClientTypes>cause.TRPCClientError<InferrableClientTypes>.data: anydata?.code === 'NOT_FOUND') { throw function notFound(options?: NotFoundError): NotFoundErrornotFound(); } throw cause: anycause; }); return {
post: unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
post
};
}, UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function PostPage(): JSX.ElementPostPage, }); function function PostPage(): JSX.ElementPostPage() { const
const loaderData: {
    post: unstable_TRPCQueryHandle<{
        input: {
            id: string;
        };
        output: {
            id: string;
            title: string;
        };
        transformer: true;
        errorShape: DefaultErrorShape;
        featureFlags: {
            keyPrefix: false;
        };
    }>;
}
loaderData
=
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id", "/posts/$id", "/posts/$id", "/posts/$id", undefined, ResolveParams<"/posts/$id">, AnyContext, AnyContext, AnyContext, {}, ({ context: { trpc }, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, ... 8 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/posts/$id", "/posts/$id">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {
    readonly PostsIdRoute: Route<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
        trpc: TRPC;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts/$id", "/posts/$id", "/posts/$id", "/posts/$id", ... 11 more ..., undefined>;
}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
return <
function Post(props: {
    post: unstable_inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post
post: unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
post
={
const loaderData: {
    post: unstable_TRPCQueryHandle<{
        input: {
            id: string;
        };
        output: {
            id: string;
            title: string;
        };
        transformer: true;
        errorShape: DefaultErrorShape;
        featureFlags: {
            keyPrefix: false;
        };
    }>;
}
loaderData
.
post: unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
post
} />;
}
  • No Suspense boundary is needed for Post: the data is in the cache before the route renders.
  • The navigation waits for the loader. On a slow request, the router shows the route's pendingComponent, if it has one.
  • The loader can act on errors, as here with notFound().
  • The handle carries no data. To use the data in the loader, for example for the route's head, read it with await context.queryClient.ensureQueryData(context.trpc.unstable_$fromHandle(post)) and return what you need. See Using the data in a loader.

To wait for some queries and stream others in the same loader, see Combining both: start every query first, then await.

6. Read handles in components

Components read handles with unstable_$fromHandle from useTRPC(). They're the same components you would write for the Next.js App Router, minus 'use client':

src/components/post.tsx
import { function useSuspenseQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseSuspenseQueryResult<TData, TError>useSuspenseQuery } from '@tanstack/react-query';
import type { type unstable_inferQueryHandle<TProcedure extends DecorateQueryHandleVerbs<any>> = TProcedure["unstable_$prefetch"] extends (...args: any) => infer R ? R : anyunstable_inferQueryHandle } from '@trpc/tanstack-react-query';
import { 
const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
useTRPC
, type
type TRPC = DecoratedRouterRecord<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>, {
    ...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
TRPC
} from '~/lib/trpc';
export function
function Post(props: {
    post: unstable_inferQueryHandle<TRPC["post"]["byId"]>;
}): JSX.Element
Post
(
props: {
    post: unstable_inferQueryHandle<TRPC["post"]["byId"]>;
}
props
: {
post: unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
post
: type unstable_inferQueryHandle<TProcedure extends DecorateQueryHandleVerbs<any>> = TProcedure["unstable_$prefetch"] extends (...args: any) => infer R ? R : anyunstable_inferQueryHandle<
type TRPC = DecoratedRouterRecord<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>, {
    ...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
TRPC
['post']['byId']> }) {
const
const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
trpc
=
function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
useTRPC
();
const
const postQuery: UseSuspenseQueryResult<{
    id: string;
    title: string;
}, ErrorOf<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>>
postQuery
=
useSuspenseQuery<{
    id: string;
    title: string;
}, ErrorOf<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, {
    id: string;
    title: string;
}, TRPCQueryKeyWithoutPrefix>(options: UseSuspenseQueryOptions<{
    id: string;
    title: string;
}, ErrorOf<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, {
    id: string;
    title: string;
}, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseSuspenseQueryResult<...>
useSuspenseQuery
(
const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
trpc
.
DecorateHandleReaders<{ keyPrefix: false; }>.unstable_$fromHandle: <{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}, {
    id: string;
    title: string;
}>(handle: unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<{
    id: string;
    title: string;
}>, {
    id: string;
    title: string;
}, ErrorOf<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, {
    ...;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
unstable_$fromHandle
(
props: {
    post: unstable_inferQueryHandle<TRPC["post"]["byId"]>;
}
props
.
post: unstable_TRPCQueryHandle<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
post
));
return <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{
const postQuery: UseSuspenseQueryResult<{
    id: string;
    title: string;
}, ErrorOf<{
    input: {
        id: string;
    };
    output: {
        id: string;
        title: string;
    };
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>>
postQuery
.
data: {
    id: string;
    title: string;
}
data
.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>;
}
route loader
├─ trpc.post.byId.unstable_$prefetch({ id })        → post handle     → <Post post={post} />
└─ trpc.post.comments.unstable_$prefetchInfinite()  → comments handle → <Comments comments={comments} />

Start every query from the URL

A loader only sees the URL, so give it everything a page's queries need: path params for what the page is about, and search params for filters, sort and page. With validateSearch and loaderDeps, the loader starts the right query on every navigation, before the route renders:

src/routes/posts.tsx
import { function useSuspenseQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseSuspenseQueryResult<TData, TError>useSuspenseQuery } from '@tanstack/react-query';
import { function createFileRoute<TFilePath extends keyof FileRoutesByPath, TParentRoute extends AnyRoute = FileRoutesByPath[TFilePath]["parentRoute"], TId extends RouteConstraints["TId"] = FileRoutesByPath[TFilePath]["id"], TPath extends RouteConstraints["TPath"] = FileRoutesByPath[TFilePath]["path"], TFullPath extends RouteConstraints["TFullPath"] = FileRoutesByPath[TFilePath]["fullPath"]>(path?: TFilePath): FileRoute<TFilePath, TParentRoute, TId, TPath, TFullPath>["createRoute"]createFileRoute } from '@tanstack/react-router';
import { import zz } from 'zod';
import { 
const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
useTRPC
} from '../lib/trpc';
export const
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        new: "new";
        top: "top";
    }>>;
    page: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>, ResolveParams<"/posts">, AnyContext, AnyContext, AnyContext, ... 6 more ..., undefined>
Route
=
createFileRoute<"/posts", RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts">(path?: "/posts" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
('/posts')({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, ... 7 more ..., undefined>, ... 12 more ..., undefined>.validateSearch?: Constrain<z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        new: "new";
        top: "top";
    }>>;
    page: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>, AnyValidator, DefaultValidator>
validateSearch
: import zz.
function object<{
    sort: z.ZodDefault<z.ZodEnum<{
        new: "new";
        top: "top";
    }>>;
    page: z.ZodDefault<z.ZodNumber>;
}>(shape?: {
    sort: z.ZodDefault<z.ZodEnum<{
        new: "new";
        top: "top";
    }>>;
    page: z.ZodDefault<z.ZodNumber>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        new: "new";
        top: "top";
    }>>;
    page: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>
object
({
sort: z.ZodDefault<z.ZodEnum<{
    new: "new";
    top: "top";
}>>
sort
: import zz.
enum<readonly ["new", "top"]>(values: readonly ["new", "top"], params?: string | z.core.$ZodEnumParams): z.ZodEnum<{
    new: "new";
    top: "top";
}> (+1 overload)
export enum
enum
(['new', 'top']).
ZodType<any, any, $ZodEnumInternals<{ new: "new"; top: "top"; }>>.default(def: "new" | "top"): z.ZodDefault<z.ZodEnum<{
    new: "new";
    top: "top";
}>> (+1 overload)
default
('new'),
page: z.ZodDefault<z.ZodNumber>page: import zz.function number(params?: string | z.core.$ZodNumberParams): z.ZodNumbernumber()._ZodNumber<$ZodNumberInternals<number>>.int(params?: string | z.core.$ZodCheckNumberFormatParams): z.ZodNumberint()._ZodNumber<$ZodNumberInternals<number>>.positive(params?: string | z.core.$ZodCheckGreaterThanParams): z.ZodNumberpositive().ZodType<any, any, $ZodNumberInternals<number>>.default(def: number): z.ZodDefault<z.ZodNumber> (+1 overload)default(1), }),
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.loaderDeps?: ((opts: FullSearchSchemaOption<RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        new: "new";
        top: "top";
    }>>;
    page: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>>) => {
    sort: "new" | "top";
    page: number;
}) | undefined
loaderDeps
: ({
search: {
    sort: "new" | "top";
    page: number;
}
search
}) =>
search: {
    sort: "new" | "top";
    page: number;
}
search
,
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context: { trpc }, deps }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", ResolveParams<"/posts">, {
    sort: "new" | "top";
    page: number;
}, AnyContext, AnyContext, AnyContext, unknown, undefined>) => {
    posts: unstable_TRPCQueryHandle<{
        input: {
            sort: "new" | "top";
            page: number;
        };
        output: {
            id: string;
            title: string;
        }[];
        transformer: true;
        errorShape: DefaultErrorShape;
        featureFlags: {
            ...;
        };
    }>;
}, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: ({
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient; trpc: TRPC; }, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient;
    trpc: TRPC;
}
context
: { trpc: TRPCtrpc },
deps: {
    sort: "new" | "top";
    page: number;
}
deps
}) => ({
posts: unstable_TRPCQueryHandle<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
posts
: trpc: TRPCtrpc.
post: DecoratedRouterRecord<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: {
            id: string;
            title: string;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    comments: QueryProcedure<{
        input: {
            postId: string;
            limit?: number | undefined;
            cursor?: number | null | undefined;
        };
        output: {
            items: {
                id: string;
                text: string;
            }[];
            nextCursor: number | null;
        };
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
    list: QueryProcedure<...>;
}>, {
    ...;
}> & DecorateRouterKeyable<...> & DecorateHandleReaders<...>
post
.
list: DecorateQueryProcedure<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> & Record<string, never>
list
.
DecorateQueryHandleVerbs<{ input: { sort: "new" | "top"; page: number; }; output: { id: string; title: string; }[]; transformer: true; errorShape: DefaultErrorShape; featureFlags: { ...; }; }>.unstable_$prefetch: (input: {
    sort: "new" | "top";
    page: number;
}, opts?: unstable_TRPCPrefetchOptions | undefined) => unstable_TRPCQueryHandle<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
unstable_$prefetch
(
deps: {
    sort: "new" | "top";
    page: number;
}
deps
),
}), UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: function PostsPage(): JSX.ElementPostsPage, }); function function PostsPage(): JSX.ElementPostsPage() { const
const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
trpc
=
function useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
useTRPC
();
const
const loaderData: {
    posts: unstable_TRPCQueryHandle<{
        input: {
            sort: "new" | "top";
            page: number;
        };
        output: {
            id: string;
            title: string;
        }[];
        transformer: true;
        errorShape: DefaultErrorShape;
        featureFlags: {
            keyPrefix: false;
        };
    }>;
}
loaderData
=
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", z.ZodObject<{
    sort: z.ZodDefault<z.ZodEnum<{
        new: "new";
        top: "top";
    }>>;
    page: z.ZodDefault<z.ZodNumber>;
}, z.core.$strip>, ResolveParams<"/posts">, AnyContext, AnyContext, AnyContext, ... 6 more ..., undefined>
Route
.
RouteExtensions<"/posts", "/posts">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient;
    trpc: TRPC;
}, AnyContext, AnyContext, {}, undefined, {
    readonly PostsRoute: Route<Register, RootRoute<Register, undefined, {
        queryClient: QueryClient;
        trpc: TRPC;
    }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/posts", "/posts", "/posts", "/posts", z.ZodObject<{
        sort: z.ZodDefault<z.ZodEnum<...>>;
        page: z.ZodDefault<...>;
    }, z.core.$strip>, ... 10 more ..., undefined>;
}, unknown, unknown, unknown, undefined>, "never", false, RouterHistory, Record<...>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<...> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
const
const postsQuery: UseSuspenseQueryResult<{
    id: string;
    title: string;
}[], ErrorOf<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>>
postsQuery
=
useSuspenseQuery<{
    id: string;
    title: string;
}[], ErrorOf<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, {
    id: string;
    title: string;
}[], TRPCQueryKeyWithoutPrefix>(options: UseSuspenseQueryOptions<{
    id: string;
    title: string;
}[], ErrorOf<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, {
    id: string;
    title: string;
}[], TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseSuspenseQueryResult<...>
useSuspenseQuery
(
const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: {
        req: Request;
    };
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: {
            req: Request;
        };
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: true;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: {
                id: string;
                title: string;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        comments: QueryProcedure<{
            input: {
                postId: string;
                limit?: number | undefined;
                cursor?: number | null | undefined;
            };
            output: {
                items: {
                    id: string;
                    text: string;
                }[];
                nextCursor: number | null;
            };
            meta: object;
            errorShape: DefaultErrorShape;
        }>;
        list: QueryProcedure<...>;
    }>>;
}>>, {
    ...;
}>
trpc
.
DecorateHandleReaders<{ keyPrefix: false; }>.unstable_$fromHandle: <{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}, {
    id: string;
    title: string;
}[]>(handle: unstable_TRPCQueryHandle<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, opts?: UnusedSkipTokenTRPCQueryOptionsIn<NoInfer<{
    id: string;
    title: string;
}[]>, {
    id: string;
    title: string;
}[], ErrorOf<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>, {
    ...;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<...> (+1 overload)
unstable_$fromHandle
(
const loaderData: {
    posts: unstable_TRPCQueryHandle<{
        input: {
            sort: "new" | "top";
            page: number;
        };
        output: {
            id: string;
            title: string;
        }[];
        transformer: true;
        errorShape: DefaultErrorShape;
        featureFlags: {
            keyPrefix: false;
        };
    }>;
}
loaderData
.
posts: unstable_TRPCQueryHandle<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>
posts
),
); return ( <React.JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul> {
const postsQuery: UseSuspenseQueryResult<{
    id: string;
    title: string;
}[], ErrorOf<{
    input: {
        sort: "new" | "top";
        page: number;
    };
    output: {
        id: string;
        title: string;
    }[];
    transformer: true;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}>>
postsQuery
.
data: {
    id: string;
    title: string;
}[]
data
.
Array<{ id: string; title: string; }>.map<JSX.Element>(callbackfn: (value: {
    id: string;
    title: string;
}, index: number, array: {
    id: string;
    title: string;
}[]) => JSX.Element, thisArg?: any): JSX.Element[]
map
((
post: {
    id: string;
    title: string;
}
post
) => (
<React.JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li React.Attributes.key?: React.Key | null | undefinedkey={
post: {
    id: string;
    title: string;
}
post
.id: stringid}>{
post: {
    id: string;
    title: string;
}
post
.title: stringtitle}</React.JSX.IntrinsicElements.li: React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>li>
))} </React.JSX.IntrinsicElements.ul: React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>ul> ); }

A link to /posts?sort=top starts the query for the top posts as the navigation begins. The back button brings back the last sort, and a shared link opens the same view. Keep the sort in useState and you lose all three, since the loader can't see component state.

/posts?sort=top&page=2  →  trpc.post.list.unstable_$prefetch({ sort: 'top', page: 2 })

If a query can't take its input from the URL, for example because it needs an id from another query's result, reshape the procedure: see If the URL isn't enough, reshape the procedure.

Handles in beforeLoad

beforeLoad can return handles too, for example to start a query in a parent route and read it in child routes. TanStack Router checks that values returned from beforeLoad are serializable, and handles pass that check as they are: their query keys and inputs are typed from the procedure.

Don't return handles from server functions

A server function runs with its own QueryClient, which never reaches the page. Return data from server functions, and start queries in loaders. In development, tRPC warns when it renders a handle whose data never reached the page's QueryClient.