Fetching data
We use tRPC with TanStack Query (React Query) for all data fetching. This setup gives full type safety from database to UI, automatic caching, loading states, error handling, and smart refetching.
Three ways to fetch data
Section titled “Three ways to fetch data”Pick the right approach based on where you’re calling from:
| Where you’re calling from | Use this | Example |
|---|---|---|
| Server Component (RSC) | trpc from @/trpc/server | await trpc.user.getInfo() |
| Client Component (React) | trpc from @/trpc/client | trpc.user.getInfo.useQuery() |
| Client outside React | trpc from @/trpc/vanilla | await trpc.user.getInfo.ensureData() |
All three share the same React Query cache, so data fetched in one place is available everywhere.
1. Server-side (RSC)
Section titled “1. Server-side (RSC)”Use when fetching data in Server Components for initial page load or prefetching.
Import:
import { trpc } from '@/trpc/server';Basic fetch:
import { trpc } from '@/trpc/server';
const DocumentPage = async ({ params }: { params: { id: string } }) => { const document = await trpc.document.getById(params.id);
return <DocumentViewer document={document} />;};Prefetch (loads data into cache before client needs it):
import { trpc, HydrateClient } from '@/trpc/server';
const Layout = async ({ children }) => { // Prefetch data - goes into React Query cache await trpc.user.getInfo.prefetch(); await trpc.credits.getUsage.prefetch();
// Client components can now access this cached data instantly return ( <HydrateClient> {children} </HydrateClient> );};When to use:
- Initial page data (SSR)
- Prefetching data you know client will need
- SEO-critical data
2. Client-side in React Components
Section titled “2. Client-side in React Components”Use when fetching data in client components. Gets automatic loading states, error handling, caching, and refetching.
Import:
import { trpc } from '@/trpc/client';Basic query:
const MyComponent = () => { const { data, isLoading, error } = trpc.library.getItems.useQuery();
if (isLoading) return <Spinner />; if (error) return <Error />;
return <List items={data} />;};Query with params:
const DocumentViewer = ({ documentId }: { documentId: string }) => { const { data: document } = trpc.document.getById.useQuery(documentId);
return <div>{document?.name}</div>;};Query with options:
const SessionInfo = ({ sessionId }: { sessionId: string }) => { const { data } = trpc.billing.getStripeSessionInfo.useQuery(sessionId, { enabled: Boolean(sessionId), // only fetch when sessionId exists staleTime: 30_000, // consider fresh for 30s refetchOnWindowFocus: false, // don't refetch on tab focus });
return <div>{data?.status}</div>;};When to use:
- Fetching data in client components
- Need loading/error states
- Want automatic refetching
- Data that updates based on user interaction
3. Client-side outside React
Section titled “3. Client-side outside React”Use when you need to fetch data outside React components (e.g., in Zustand stores, event handlers, utility functions).
Import:
import { trpc } from '@/trpc/vanilla';Available methods:
| Method | When to use | Behavior |
|---|---|---|
ensureData() | Default choice | Returns cache if available, fetches if missing |
fetch() | Need fresh data | Always hits server, updates cache |
getData() | Only want cached | Returns cache or undefined, never fetches |
ensureData (recommended):
// Zustand store actionimport { trpc } from '@/trpc/vanilla';
export const ThemeActions = { loadSettings: async () => { // Uses cache if available, fetches if not const settings = await trpc.user.getThemeSettings.ensureData(); setState({ settings }); },};fetch (always fresh):
// When you need guaranteed fresh dataconst handleRefresh = async () => { // Always hits server const user = await trpc.user.getInfo.fetch(); console.log('Fresh user data:', user);};getData (cache only):
// When you only want to check cacheconst getCachedUser = () => { // Returns data or undefined, never fetches const user = trpc.user.getInfo.getData(); if (user) { console.log('Found in cache:', user); }};Real examples:
// Zustand store - checking user before actionexport const GenerateActions = { startGenerating: async () => { const user = await trpc.user.getInfo.ensureData(); if (!user) return; // ... rest of logic },};
// Event handler outside componentconst handleE2EHelper = async () => { const outline = await trpc.presentation.getOutlineByPresentationId.ensureData(id); return outline?.content;};When to use:
- Zustand store actions
- Event handlers
- Non-React utility functions
- Background tasks
Invalidating queries
Section titled “Invalidating queries”Invalidate queries after mutations to keep UI in sync with server.
In React:
const utils = trpc.useUtils();
// Invalidate specific queryutils.user.getInfo.invalidate();
// Invalidate all user queriesutils.user.invalidate();Outside React:
import { trpc } from '@/trpc/vanilla';
// Invalidate specific queryawait trpc.user.getInfo.invalidate();
// Invalidate all user queriesawait trpc.user.invalidate();When to invalidate:
- After create/update/delete mutations
- After user actions that change server state
- When data changes outside React Query’s control