Capítulo 19 de 24
This example wires a tRPC client into TanStack Router through React Query, using createTRPCOptionsProxy to generate queryOptions-compatible objects directly from a typed tRPC AppRouter. Route loaders and components consume the same tRPC-derived query options, giving end-to-end type safety from the Express server to the route component.
createFileRoute, routes under src/routes/, route tree generated into routeTree.gen.ts)@tanstack/react-router, @tanstack/router-plugin, @tanstack/react-query, @trpc/client, @trpc/server, @trpc/tanstack-react-query, express, zodsrc/router.tsx creates the QueryClient, builds a trpc proxy with createTRPCOptionsProxy<AppRouter>, and passes both into router context. src/server/trpc.ts defines the tRPC appRouter (procedures hello, posts, post) and an Express middleware. Route files like src/routes/dashboard.posts.$postId.tsx use trpc.post.queryOptions(postId) in both the loader and useQuery.// src/router.tsx
export const queryClient = new QueryClient()
export const trpc = createTRPCOptionsProxy<AppRouter>({
client: createTRPCClient({
links: [httpBatchLink({ url: '/trpc' })],
}),
queryClient,
})
export function createRouter() {
return createTanStackRouter({
routeTree,
context: { trpc, queryClient },
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
}
// src/routes/dashboard.posts.$postId.tsx
export const Route = createFileRoute('/dashboard/posts/$postId')({
loader: async ({ context: { trpc, queryClient }, params: { postId } }) => {
await queryClient.ensureQueryData(trpc.post.queryOptions(postId))
},
component: DashboardPostsPostIdComponent,
})
function DashboardPostsPostIdComponent() {
const postId = Route.useParams({ select: (d) => d.postId })
const postQuery = useQuery(trpc.post.queryOptions(postId))
// ...
}
trpc.<procedure>.queryOptions(input) replaces hand-written queryOptions() factories, deriving query keys and fetchers straight from the tRPC router's procedure signatures.createTRPCOptionsProxy needs the same queryClient instance passed to QueryClientProvider, so create it once in router.tsx and thread it through router context rather than instantiating a second QueryClient.trpc.post.queryOptions(postId) is called identically in the loader and the component, there is no manual query-key bookkeeping, unlike the raw queryOptions() approach in the basic example.Route.useNavigate() combined with validateSearch (using zod) lets URL search params double as UI state (e.g. the notes textarea persisted via search.notes).src/server/trpc.ts) and Vite client share one AppRouter type export, so a change to a procedure's input/output is a compile error on the client immediately.queryOptions() factories there against trpc.post.queryOptions() here to see what tRPC's proxy automates.