Skip to content

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.

Pick the right approach based on where you’re calling from:

Where you’re calling fromUse thisExample
Server Component (RSC)trpc from @/trpc/serverawait trpc.user.getInfo()
Client Component (React)trpc from @/trpc/clienttrpc.user.getInfo.useQuery()
Client outside Reacttrpc from @/trpc/vanillaawait trpc.user.getInfo.ensureData()

All three share the same React Query cache, so data fetched in one place is available everywhere.


Use when fetching data in Server Components for initial page load or prefetching.

Import:

import { trpc } from '@/trpc/server';

Basic fetch:

app/document/[id]/page.tsx
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):

app/layout.tsx
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

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

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:

MethodWhen to useBehavior
ensureData()Default choiceReturns cache if available, fetches if missing
fetch()Need fresh dataAlways hits server, updates cache
getData()Only want cachedReturns cache or undefined, never fetches

ensureData (recommended):

// Zustand store action
import { 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 data
const 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 cache
const 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 action
export const GenerateActions = {
startGenerating: async () => {
const user = await trpc.user.getInfo.ensureData();
if (!user) return;
// ... rest of logic
},
};
// Event handler outside component
const 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

Invalidate queries after mutations to keep UI in sync with server.

In React:

const utils = trpc.useUtils();
// Invalidate specific query
utils.user.getInfo.invalidate();
// Invalidate all user queries
utils.user.invalidate();

Outside React:

import { trpc } from '@/trpc/vanilla';
// Invalidate specific query
await trpc.user.getInfo.invalidate();
// Invalidate all user queries
await 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