Set up @trpc/tanstack-react-query with createTRPCContext(), TRPCProvider, useTRPC() hook, queryOptions/mutationOptions factories, query invalidation via queryClient.invalidateQueries with queryFilter, and type inference with inferInput/inferOutput.
This skill builds on [client-setup] and [links]. Read them first for foundational concepts.
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query
import { createTRPCContext } from '@trpc/tanstack-react-query';
import type { AppRouter } from '../server/router';
export const { TRPCProvider, useTRPC, useTRPCClient } =
createTRPCContext<AppRouter>();
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import type { AppRouter } from '../server/router';
import { TRPCProvider } from '../utils/trpc';
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (typeof window === 'undefined') {
return makeQueryClient();
}
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
export function App() {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000/api/trpc',
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{/* Your app here */}
</TRPCProvider>
</QueryClientProvider>
);
}
import { QueryClient } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import type { AppRouter } from '../server/router';
export const queryClient = new QueryClient();
const trpcClient = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })],
});
export const trpc = createTRPCOptionsProxy<AppRouter>({
client: trpcClient,
queryClient,
});
When using the singleton pattern, wrap your app with QueryClientProvider only (no TRPCProvider needed) and import trpc directly instead of calling useTRPC().
import { skipToken, useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { useTRPC } from '../utils/trpc';
function UserList() {
const trpc = useTRPC();
// Basic query
const userQuery = useQuery(trpc.user.byId.queryOptions({ id: '1' }));
// With TanStack Query options
const staleQuery = useQuery(
trpc.user.byId.queryOptions({ id: '1' }, { staleTime: 5000 }),
);
// Suspense query
const { data } = useSuspenseQuery(trpc.user.byId.queryOptions({ id: '1' }));
// Conditional query with skipToken
const conditionalQuery = useQuery(
trpc.user.byId.queryOptions(userId ? { id: userId } : skipToken),
);
return <div>{userQuery.data?.name}</div>;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '../utils/trpc';
function CreateUser() {
const trpc = useTRPC();
const queryClient = useQueryClient();
const createUser = useMutation(
trpc.user.create.mutationOptions({
onSuccess: () => {
queryClient.invalidateQueries(trpc.user.queryFilter());
},
}),
);
return (
<button onClick={() => createUser.mutate({ name: 'Alice' })}>Create</button>
);
}
import { useQueryClient } from '@tanstack/react-query';
import { useTRPC } from '../utils/trpc';
function InvalidationExample() {
const trpc = useTRPC();
const queryClient = useQueryClient();
// Invalidate a specific query
const invalidateOne = () =>
queryClient.invalidateQueries(trpc.user.byId.queryFilter({ id: '1' }));
// Invalidate all queries under a router
const invalidateAll = () =>
queryClient.invalidateQueries(trpc.user.queryFilter());
// Invalidate ALL tRPC queries
const invalidateEverything = () =>
queryClient.invalidateQueries({ queryKey: trpc.pathKey() });
// Read/write cache directly
const cached = queryClient.getQueryData(trpc.user.byId.queryKey({ id: '1' }));
queryClient.setQueryData(trpc.user.byId.queryKey({ id: '1' }), {
id: '1',
name: 'Updated',
});
}
import { useInfiniteQuery } from '@tanstack/react-query';
import { useTRPC } from '../utils/trpc';
function InfiniteList() {
const trpc = useTRPC();
const posts = useInfiniteQuery(
trpc.post.list.infiniteQueryOptions(
{ limit: 10 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
},
),
);
return (
<div>
{posts.data?.pages.flatMap((page) =>
page.items.map((item) => <div key={item.id}>{item.title}</div>),
)}
<button onClick={() => posts.fetchNextPage()}>Load more</button>
</div>
);
}
import { useSubscription } from '@trpc/tanstack-react-query';
import { useTRPC } from '../utils/trpc';
function ChatMessages() {
const trpc = useTRPC();
const sub = useSubscription(
trpc.chat.onMessage.subscriptionOptions(
{ channelId: 'general' },
{
onData: (message) => {
console.log('New message:', message);
},
onError: (err) => {
console.error('Subscription error:', err);
},
},
),
);
return (
<div>
Status: {sub.status}, Last: {JSON.stringify(sub.data)}
</div>
);
}
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { inferInput, inferOutput } from '@trpc/tanstack-react-query';
import type { AppRouter } from '../server/router';
// Router-level inference
type Inputs = inferRouterInputs<AppRouter>;
type Outputs = inferRouterOutputs<AppRouter>;
type UserInput = Inputs['user']['byId'];
type UserOutput = Outputs['user']['byId'];
// Procedure-level inference (inside component)
// const trpc = useTRPC();
// type Input = inferInput<typeof trpc.user.byId>;
// type Output = inferOutput<typeof trpc.user.byId>;
import { useTRPCClient } from '../utils/trpc';
async function DirectCall() {
const client = useTRPCClient();
const result = await client.user.byId.query({ id: '1' });
}
Calling useQuery({ queryKey: [...], queryFn: ... }) manually bypasses tRPC's type-safe query key generation and loses autocomplete. Always use the options factory.
// WRONG
useQuery({ queryKey: ['user', id], queryFn: () => fetch(...) });
// CORRECT
const trpc = useTRPC();
useQuery(trpc.user.byId.queryOptions({ id }));
useTRPC() throws "can only be used inside a <TRPCProvider>" if the component tree is not wrapped. Ensure TRPCProvider is mounted above all components that call useTRPC(), typically in your root App component.
The new @trpc/tanstack-react-query package does not use utils.invalidate(). Use queryClient.invalidateQueries() with queryFilter() instead.
// WRONG -- classic pattern does not work with new package
utils.post.invalidate();
// CORRECT
const trpc = useTRPC();
const queryClient = useQueryClient();
queryClient.invalidateQueries(trpc.post.queryFilter());
In server-rendered apps, a singleton QueryClient leaks data between requests. Always create a new QueryClient per request on the server, and reuse a single instance only in the browser.
// WRONG
const queryClient = new QueryClient(); // shared across requests!
// CORRECT
function getQueryClient() {
if (typeof window === 'undefined') return makeQueryClient();
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}