Capítulo 19 de 24

Chapter 19: tRPC + React Query Integration

Core Idea

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.

Setup

  • Routing style: file-based (createFileRoute, routes under src/routes/, route tree generated into routeTree.gen.ts)
  • Key dependencies: @tanstack/react-router, @tanstack/router-plugin, @tanstack/react-query, @trpc/client, @trpc/server, @trpc/tanstack-react-query, express, zod
  • Structure: src/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.

Code Example

// 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))
  // ...
}
  • What it demonstrates: trpc.<procedure>.queryOptions(input) replaces hand-written queryOptions() factories, deriving query keys and fetchers straight from the tRPC router's procedure signatures.

Key Takeaways

  1. 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.
  2. Because 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.
  3. Route.useNavigate() combined with validateSearch (using zod) lets URL search params double as UI state (e.g. the notes textarea persisted via search.notes).
  4. The Express server (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.

Connects To

  • basic-react-query (ch018): the non-tRPC baseline; compare queryOptions() factories there against trpc.post.queryOptions() here to see what tRPC's proxy automates.
  • router-monorepo-react-query (ch024): another way to centralize query definitions, but via a dedicated workspace package instead of a tRPC proxy.