The server is ready, and now we have to connect the interface. We need to show "loading" and errors, not send the same requests a hundred times, and refresh the list after a note is added. TanStack Query already takes care of all this, and tRPC neatly connects it to your router — with full types. In this lesson we will connect the client to the Next.js App Router, learn to read and change data, make optimistic updates and call procedures directly on the server.

How the integration works

In the first lesson we called procedures "directly": trpc.note.list.query(). That is the plain client — it can send a request and return the result, but it knows nothing about the cache or the loading state. For React there is the @trpc/tanstack-react-query package, and it works differently: it does not replace TanStack Query, it prepares options for it.

An analogy: tRPC is the "recipe" of a request (the address, the input, the result type), and TanStack Query is the "cook" who prepares it by the recipe: caches, retries on failure, shows the loading state. The methods queryOptions() and mutationOptions() return the recipe, and you hand it to the cook yourself — into the ordinary useQuery and useMutation.

In tRPCIn TanStack QueryWhat for
trpc.x.queryOptions()useQueryreading data
trpc.x.mutationOptions()useMutationchanging data
trpc.x.queryFilter()invalidateQueriesreset the cache so the data loads again
trpc.x.queryKey()getQueryData, setQueryDataedit the cache by hand

Setup: the provider

We need to create two objects — a TanStack Query client (QueryClient) and a tRPC client — and pass them to a provider. The hooks and the provider are created by the createTRPCContext function:

tsx
// lib/trpc.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import type { AppRouter } from '@/server/routers/_app';

export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();

export function TRPCReactProvider(props: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    createTRPCClient<AppRouter>({
      links: [httpBatchLink({ url: '/api/trpc' })],
    }),
  );

  return (
    <QueryClientProvider client={queryClient}>
      <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
        {props.children}
      </TRPCProvider>
    </QueryClientProvider>
  );
}

What matters here:

  • 'use client' is required: the provider and the hooks work only in client components.
  • createTRPCContext<AppRouter>() returns TRPCProvider and the useTRPC hook. The AppRouter type is the very one the server exported in the previous lesson.
  • Both clients are created with useState(() => ...) so that they are not recreated on every render: otherwise the cache would be reset.
  • url must match endpoint in the route handler. If the server has transformer: superjson turned on, add it to httpBatchLink as well.