Migrate from @trpc/react-query (classic) to @trpc/tanstack-react-query. Run npx @trpc/upgrade CLI for automated codemod. Manually migrate remaining patterns: hook-based to options-factory, utils.invalidate to queryClient.invalidateQueries with queryFilter, provider changes.
This skill builds on [react-query-setup]. Read it first for foundational concepts.
This skill covers migrating from @trpc/react-query (the classic tRPC React hooks) to @trpc/tanstack-react-query (the new options-factory client). The two packages can coexist in the same application, so you can migrate incrementally.
npx @trpc/upgrade
When prompted, select:
Migrate Hooks to queryOptions/mutationOptions APIMigrate context provider setupThe codemod handles common patterns but is a work in progress. Always typecheck after running it.
npm install @trpc/tanstack-react-query
You can keep @trpc/react-query installed during the migration period.
Replace the classic createTRPCReact setup with createTRPCContext:
// BEFORE (classic)
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router';
export const trpc = createTRPCReact<AppRouter>();
// AFTER (new)
import { createTRPCContext } from '@trpc/tanstack-react-query';
import type { AppRouter } from '../server/router';
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
Update the provider in your app root:
// BEFORE (classic)
// trpc.Provider wrapping with queryClient and client props
// AFTER (new)
import { QueryClientProvider } from '@tanstack/react-query';
import { TRPCProvider } from '../utils/trpc';
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{children}
</TRPCProvider>
</QueryClientProvider>;
// BEFORE (classic)
import { trpc } from './trpc';
function Users() {
const greeting = trpc.greeting.useQuery({ name: 'Jerry' });
}
// AFTER (new)
import { useQuery } from '@tanstack/react-query';
import { useTRPC } from './trpc';
function Users() {
const trpc = useTRPC();
const greeting = useQuery(trpc.greeting.queryOptions({ name: 'Jerry' }));
}
// BEFORE (classic)
import { trpc } from './trpc';
function Users() {
const createUser = trpc.createUser.useMutation();
createUser.mutate({ name: 'Jerry' });
}
// AFTER (new)
import { useMutation } from '@tanstack/react-query';
import { useTRPC } from './trpc';
function Users() {
const trpc = useTRPC();
const createUser = useMutation(trpc.createUser.mutationOptions());
createUser.mutate({ name: 'Jerry' });
}
// BEFORE (classic)
import { trpc } from './trpc';
function Users() {
const utils = trpc.useUtils();
async function invalidateGreeting() {
await utils.greeting.invalidate({ name: 'Jerry' });
}
}
// AFTER (new)
import { useQueryClient } from '@tanstack/react-query';
import { useTRPC } from './trpc';
function Users() {
const trpc = useTRPC();
const queryClient = useQueryClient();
async function invalidateGreeting() {
await queryClient.invalidateQueries(
trpc.greeting.queryFilter({ name: 'Jerry' }),
);
}
}
Any classic trpc.useUtils() usage maps to standard TanStack Query useQueryClient() calls:
Classic (utils.xxx) | New (queryClient + trpc) |
|---|---|
utils.post.invalidate() | queryClient.invalidateQueries(trpc.post.queryFilter()) |
utils.post.refetch() | queryClient.refetchQueries(trpc.post.queryFilter()) |
utils.post.getData(input) | queryClient.getQueryData(trpc.post.byId.queryKey(input)) |
utils.post.setData(input,d) | queryClient.setQueryData(trpc.post.byId.queryKey(input),d) |
After migrating all files (or a batch of files), run TypeScript to catch remaining issues:
npx tsc --noEmit
Common type errors after migration:
useTRPC() call (the new pattern requires calling the hook inside the component)queryOptions takes the procedure input as the first arg; mutationOptions takes TanStack Query options like onSuccess/onError, with mutation variables passed later to mutate(...))useUtils() references that need to become useQueryClient() + useTRPC() pairsOnce all files are migrated and TypeScript passes:
npm uninstall @trpc/react-query
npx @trpc/upgrade is a work-in-progress codemod. It handles common patterns but may miss complex cases like dynamic query keys, conditional hooks, or custom wrappers around tRPC hooks. Always run tsc --noEmit after the codemod completes and fix remaining errors manually.
While the classic and new packages can coexist in the same app, mixing their hooks in the same component creates confusing dual-provider requirements and makes the code harder to reason about. Migrate one component at a time, converting all hooks in that component from classic to new in a single pass.