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 tRPC | In TanStack Query | What for |
|---|---|---|
trpc.x.queryOptions() | useQuery | reading data |
trpc.x.mutationOptions() | useMutation | changing data |
trpc.x.queryFilter() | invalidateQueries | reset the cache so the data loads again |
trpc.x.queryKey() | getQueryData, setQueryData | edit 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:
// 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>()returnsTRPCProviderand theuseTRPChook. TheAppRoutertype 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. urlmust matchendpointin the route handler. If the server hastransformer: superjsonturned on, add it tohttpBatchLinkas well.