Client Configuration
How tRPC connects to the server from React components -- the client setup, provider tree, and hook usage patterns.
The client side of the API layer connects React components to the tRPC server using TanStack Query. All server data in the application flows through tRPC hooks.
The tRPC React Client
The tRPC React client is defined in apps/web/src/lib/trpc-react.ts:
"use client";
import { httpBatchLink } from "@trpc/client";
import { type CreateTRPCReact, createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@repo/api";
export const trpc: CreateTRPCReact<AppRouter, unknown> = createTRPCReact<AppRouter>();
export function makeTRPCClient() {
return trpc.createClient({
links: [
httpBatchLink({
url: "/api/trpc",
}),
],
});
}
Key details:
createTRPCReact<AppRouter>()creates typed hooks from theAppRoutertype exported by@repo/api. This is the end-to-end type safety link -- any change to a router's input or output is immediately reflected in the client types.httpBatchLinkbatches multiple tRPC calls in the same render cycle into a single HTTP request. The URL is relative (/api/trpc) so cookies are sent automatically -- nocredentialsconfiguration needed.- The
"use client"directive marks this module as client-only sincecreateTRPCReactuses React context internally.
The Provider Tree
The Providers component in apps/web/src/components/providers.tsx wraps the tRPC client and TanStack Query client:
"use client";
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { makeTRPCClient, trpc } from "@/lib/trpc-react";
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000 },
},
}),
);
const [trpcClient] = useState(makeTRPCClient);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
}
Both clients are created inside useState to ensure they are stable across re-renders (standard React pattern for avoiding re-creation).
The QueryClient is configured with staleTime: 30_000 (30 seconds). This means data fetched by any tRPC query hook is considered fresh for 30 seconds -- re-rendering a component or navigating back to a page will serve cached data without refetching.
Providers is rendered in the app layout for authenticated pages only, not in the root layout. Public pages (auth, docs, wiki) do not need tRPC.
Using tRPC Hooks
Queries
"use client";
import { trpc } from "@/lib/trpc-react";
function MemberList() {
const { data, isLoading, error } = trpc.member.list.useQuery();
if (isLoading) return <Skeleton />;
if (error) return <ErrorDisplay error={error} />;
return (
<ul>
{data?.map((m) => (
<li key={m.id}>{m.user.name}</li>
))}
</ul>
);
}
The data type is inferred from the router's return type. For trpc.member.list.useQuery(), TypeScript knows data is the array of members with their user relations.
Queries with Input
const { data } = trpc.researchPlan.list.useQuery({
limit: 10,
offset: 0,
status: "executing",
});
The input object is type-checked against the Zod schema defined in the router's .input(). If you pass an invalid field or wrong type, TypeScript catches it at compile time.
Mutations
function OrgSettings() {
const utils = trpc.useUtils();
const updateOrg = trpc.organization.update.useMutation({
onSuccess: () => {
utils.organization.detail.invalidate();
},
});
const handleSave = (name: string) => {
updateOrg.mutate({ name });
};
return (
<button
onClick={() => handleSave("New Name")}
disabled={updateOrg.isPending}
>
Save
</button>
);
}
After a successful mutation, use utils.<router>.<procedure>.invalidate() to refetch related queries. This is TanStack Query's standard cache invalidation pattern, surfaced through tRPC's typed utilities.
Mutation State
The mutation hook provides state for loading and error UI:
updateOrg.isPending-- true while the mutation is in flightupdateOrg.isError-- true if the mutation failedupdateOrg.error-- the error object (includesdata.zodErrorfor validation failures)
State Management Context
tRPC hooks handle server state (data from the API). The application uses three other tools for different types of state:
| Tool | Use For |
|---|---|
| tRPC / TanStack Query | Server data (fetched from API, cached, invalidated) |
| Zustand | Client-only state (sidebar open, theme, UI preferences) |
| nuqs | URL search params (table filters, sort, pagination) |
| React Hook Form | Form state (inputs, validation, dirty tracking) |
Zustand, nuqs, and React Hook Form are in the dependency catalog but are installed on-demand as features require them. tRPC is the only one wired and active today.
Testing with tRPC
For integration testing, use createCaller to call router procedures without HTTP:
import { appRouter, createContext } from "@repo/api";
const caller = appRouter.createCaller({
session: mockSession,
headers: new Headers(),
logger: mockLogger,
});
const result = await caller.organization.detail();
This bypasses the HTTP layer but still runs through the full middleware chain (auth checks, tenant context, CASL).