End-to-end typesafe tRPC queries in React with httpBatchLink and createTRPCReact
tRPC v10 with createTRPCReact and httpBatchLink lets React call backend procedures with full TypeScript inference. Define an AppRouter on the server, type the client to it, and use inferred useQuery/useMutation hooks.
03 May 2026, 00:28 UTC

Useful answer
If you want React components to call backend procedures without maintaining a separate API contract, tRPC v10 with @trpc/react-query gives you full TypeScript inference from server router to client hooks. Define an AppRouter on the server, create a client with httpBatchLink pointing at /trpc, and wrap the app with createTRPCReact. The hooks infer input and output types from the router, so a rename or input change on the server becomes a compile error on the client.
Mechanism with one worked configuration
Server router defines the contract. The router type is the source of truth that is shared with the client.
// server/router.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const appRouter = t.router({
post: t.router({
byId: t.procedure.input(z.object({ id: z.number() })).query(({ input }) => {
return { id: input.id, title: 'Example' }
}),
create: t.procedure.input(z.object({ title: z.string() })).mutation(({ input }) => {
return { id: 1, title: input.title }
})
})
})
export type AppRouter = typeof appRouter
Client setup creates a tRPC React instance typed to AppRouter and connects via httpBatchLink. httpBatchLink batches multiple procedure calls into a single POST to /trpc.
// client/trpc.ts
import { httpBatchLink } from '@trpc/client'
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '../server/router'
export const trpc = createTRPCReact<AppRouter>()
export const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: 'http://localhost:3000/trpc'
})
]
})
Provider and component usage. The provider supplies the client to React Query. Hooks are inferred from AppRouter.
// app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { trpc, trpcClient } from './client/trpc'
const queryClient = new QueryClient()
export default function App() {
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Post id={1} />
</QueryClientProvider>
</trpc.Provider>
)
}
function Post({ id }: { id: number }) {
const { data, isLoading } = trpc.post.byId.useQuery({ id })
const create = trpc.post.create.useMutation()
return <div>{isLoading ? 'loading' : data?.title}</div>
}
Type flow: AppRouter is passed as generic to createTRPCReact. The resulting useQuery and useMutation signatures are derived from the procedure definitions. Changing the input schema on the server changes the required argument on the client at typecheck time.
Limits
tRPC is HTTP/JSON based. Core does not provide browser-native WebSocket or real-time subscriptions. httpBatchLink combines calls that mount in the same tick into one POST with a JSON array body, but it does not guarantee ordering across separate requests.
Errors are serialized via TRPCError and returned in the HTTP response body. They should be handled through React Query error states or an errorFormatter, not assumed to map one-to-one to HTTP status codes.
Server-side rendering requires a fresh tRPC client and QueryClient per request to avoid leaking cache or context between users.
Common mistakes
Passing the wrong router type to createTRPCReact breaks inference silently until TypeScript reports mismatches.
Calling procedures outside a trpc.Provider without a client instance results in runtime errors.
Forgetting to include context in the server caller when procedures rely on auth or request metadata leads to undefined context.
Treat tRPC errors as standard fetch errors. Use the error field from useQuery/useMutation and configure errorFormatter if you need custom serialization.
Practical verification
Run tsc --noEmit in a workspace that shares the router package. Renaming post.byId to post.getById should produce a TypeScript error at the client call site.
Open the browser Network tab and confirm calls to /trpc use POST with a JSON array body when multiple queries mount together.
Change a procedure input type in the router and confirm the client call site reports a type error before runtime.
Inspect React Query devtools to verify cache keys match the procedure path and input passed to useQuery.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.