@trpc/tanstack-react-query
Version:
TanStack React Query Integration for tRPC
348 lines (271 loc) • 9.41 kB
Markdown
---
name: react-query-setup
description: >
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.
type: framework
library: trpc
framework: react
library_version: '11.16.0'
requires:
- client-setup
- links
sources:
- www/docs/client/tanstack-react-query/overview.md
- www/docs/client/tanstack-react-query/setup.mdx
- www/docs/client/tanstack-react-query/usage.mdx
- packages/tanstack-react-query/src/
---
This skill builds on [client-setup] and [links]. Read them first for foundational concepts.
# tRPC -- TanStack React Query Setup
## Setup
### Install
```sh
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query
```
### Create the tRPC context (with React context)
```ts title="utils/trpc.ts"
import { createTRPCContext } from '@trpc/tanstack-react-query';
import type { AppRouter } from '../server/router';
export const { TRPCProvider, useTRPC, useTRPCClient } =
createTRPCContext<AppRouter>();
```
### Wire up providers
```tsx title="components/App.tsx"
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>
);
}
```
### Alternative: setup without React context (SPA / Vite)
```ts title="utils/trpc.ts"
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()`.
## Core Patterns
### queryOptions -- querying data
```tsx title="components/UserList.tsx"
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>;
}
```
### mutationOptions -- writing data
```tsx
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>
);
}
```
### Query invalidation and cache manipulation
```tsx
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',
});
}
```
### infiniteQueryOptions -- paginated data
```tsx
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>
);
}
```
### subscriptionOptions -- real-time data
```tsx
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>
);
}
```
### Type inference
```ts
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>;
```
### Accessing the tRPC client directly
```tsx
import { useTRPCClient } from '../utils/trpc';
async function DirectCall() {
const client = useTRPCClient();
const result = await client.user.byId.query({ id: '1' });
}
```
## Common Mistakes
### Using useQuery without the queryOptions factory
Calling `useQuery({ queryKey: [...], queryFn: ... })` manually bypasses tRPC's type-safe query key generation and loses autocomplete. Always use the options factory.
```tsx
// WRONG
useQuery({ queryKey: ['user', id], queryFn: () => fetch(...) });
// CORRECT
const trpc = useTRPC();
useQuery(trpc.user.byId.queryOptions({ id }));
```
### Missing TRPCProvider wrapper
`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.
### Invalidating queries with the classic utils pattern
The new `@trpc/tanstack-react-query` package does not use `utils.invalidate()`. Use `queryClient.invalidateQueries()` with `queryFilter()` instead.
```tsx
// WRONG -- classic pattern does not work with new package
utils.post.invalidate();
// CORRECT
const trpc = useTRPC();
const queryClient = useQueryClient();
queryClient.invalidateQueries(trpc.post.queryFilter());
```
### Creating a singleton QueryClient for SSR
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.
```ts
// WRONG
const queryClient = new QueryClient(); // shared across requests!
// CORRECT
function getQueryClient() {
if (typeof window === 'undefined') return makeQueryClient();
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
```
## See Also
- [client-setup] -- vanilla tRPC client, links, and transport configuration
- [links] -- httpBatchLink, splitLink, and other link types
- [react-query-classic-migration] -- migrating from @trpc/react-query to @trpc/tanstack-react-query
- [nextjs-app-router] -- using this integration with Next.js App Router and RSC