Skip to main content

tRPC v11: What's New and Should You Upgrade?

·PkgPulse Team

TL;DR

tRPC v11 is a solid upgrade if you're on v10 — React Query v5 integration is better, streaming works, and the API surface is cleaner. But the bigger question in 2026 is "should I use tRPC at all?" For TypeScript monorepos and full-stack apps (Next.js, SvelteKit, Remix): yes, absolutely — end-to-end type safety without code generation is a genuine superpower. For teams that have tried tRPC and found it adds complexity over Server Actions or REST: the alternatives are better than they were. Upgrade from v10 to v11 with confidence; new projects should evaluate tRPC, Server Actions, and REST+Zod to find the right fit.

Key Takeaways

  • React Query v5: tRPC v11 requires React Query v5 — if you're on v4, that's a double migration
  • Streaming procedures: httpSubscription link for server-sent events, observable for complex streaming
  • Improved TypeScript perf: v11 has faster type checking than v10 for large routers
  • New useSuspenseQuery: Suspense-compatible data fetching via tRPC hooks
  • Breaking changes: minor API changes; upgrade path is documented and manageable

What Changed in tRPC v11

// ─── React Query v5 Integration ───
// tRPC v10 used React Query v4 internals
// tRPC v11 requires React Query v5 (@tanstack/react-query ^5.0.0)

// Key difference in v5 query API:
// v4:
const { data, isLoading, isError } = trpc.user.get.useQuery({ id: '123' });

// v5 (same, but error handling changed):
const { data, isPending, error } = trpc.user.get.useQuery({ id: '123' });
// isLoading renamed to isPending in some contexts
// error now typed properly

// ─── Suspense Support ───
// v11 adds first-class Suspense integration:
function UserProfile({ userId }: { userId: string }) {
  // useSuspenseQuery — throws promise if loading, throws error if failed
  const [user] = trpc.user.get.useSuspenseQuery({ id: userId });
  // No data?.name — data is always defined here (Suspense handles the rest)
  return <h1>{user.name}</h1>;
}

// Wrap in Suspense + ErrorBoundary:
<ErrorBoundary fallback={<ErrorMessage />}>
  <Suspense fallback={<Skeleton />}>
    <UserProfile userId="123" />
  </Suspense>
</ErrorBoundary>

// ─── Server-Sent Events (new httpSubscription link) ───
// v10 had WebSocket subscriptions only
// v11 adds HTTP streaming via SSE (simpler, works with Vercel/Cloudflare):

// Server:
const router = t.router({
  liveUpdates: t.procedure
    .input(z.object({ userId: z.string() }))
    .subscription(async function* ({ input }) {
      // Yield values over time
      while (true) {
        const update = await waitForUpdate(input.userId);
        yield update;
        if (update.done) break;
      }
    }),
});

// Client:
const { data } = trpc.liveUpdates.useSubscription(
  { userId: '123' },
  { onData: (data) => console.log('Got update:', data) }
);

The Core tRPC Value Proposition (Still True)

// Why tRPC remains compelling in 2026:
// End-to-end type safety without code generation

// Server — define the API:
import { z } from 'zod';
import { initTRPC, TRPCError } from '@trpc/server';

const t = initTRPC.context<Context>().create();

export const appRouter = t.router({
  user: t.router({
    get: t.procedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input, ctx }) => {
        const user = await ctx.db.user.findUnique({ where: { id: input.id } });
        if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
        return user;
      }),

    update: t.procedure
      .input(z.object({
        id: z.string(),
        name: z.string().min(1).max(100),
        email: z.string().email(),
      }))
      .mutation(async ({ input, ctx }) => {
        return ctx.db.user.update({
          where: { id: input.id },
          data: { name: input.name, email: input.email },
        });
      }),
  }),
});

export type AppRouter = typeof appRouter;

// Client — fully typed, no manual type imports:
const utils = trpc.useUtils();
const { data: user } = trpc.user.get.useQuery({ id: '123' });
const updateUser = trpc.user.update.useMutation({
  onSuccess: () => utils.user.get.invalidate({ id: '123' }),
});

// user.name — TypeScript knows the shape
// updateUser.mutate({ id: '123', email: 'bad-email' }) — TypeScript error!
// Rename a field on the server? TypeScript finds every broken client usage.

// What code generation would look like (OpenAPI/GraphQL alternative):
// 1. Define schema in some format
// 2. Run codegen script
// 3. Import generated types
// 4. Keep in sync when schema changes
// tRPC: skip all of this. Types flow automatically via TypeScript inference.

tRPC vs Server Actions (2026)

// The rise of Server Actions as an alternative to tRPC:

// Server Action (Next.js 15):
'use server';
export async function updateUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  // No input validation type safety — you cast manually
  await db.user.update({ where: { id: userId }, data: { name, email } });
}

// tRPC mutation:
const updateUser = t.procedure
  .input(z.object({ name: z.string().min(1), email: z.string().email() }))
  .mutation(async ({ input }) => {
    await db.user.update({ where: { id: userId }, data: input });
  });
// input is fully typed AND validated by Zod before your code runs

// When Server Actions beat tRPC:
// → Simple form submissions (the progressive enhancement story is excellent)
// → When you want less client-side complexity
// → When you're already deep in Next.js App Router
// → "Call my backend function from my React component" use case

// When tRPC beats Server Actions:
// → Complex query patterns with filters, pagination, search
// → Multiple client types (web + mobile + CLI)
// → Teams that want full Zod validation on all inputs
// → Apps with complex client-side state (useQuery cache, optimistic updates)
// → When you need the React Query cache layer (stale-while-revalidate, etc.)

// The honest take:
// Server Actions and tRPC solve slightly different problems
// Simple CRUD mutations → Server Actions are simpler
// Complex API layer with multiple consumers → tRPC wins
// Many teams use BOTH: tRPC for queries, Server Actions for mutations

v10 → v11 Migration

# Step 1: Update dependencies
npm install @trpc/server@^11 @trpc/client@^11 @trpc/react-query@^11
npm install @tanstack/react-query@^5  # Required for v11

# Step 2: Update React Query v4 → v5 usage
# The main breaking changes in React Query v5:
# → isLoading → isPending (for queries in fetching state)
# → data type changes (no more optional in some contexts)
# → onSuccess/onError/onSettled callbacks removed from useQuery
#   (use mutation callbacks instead, or useEffect)

# v4 pattern (deprecated in v5):
const { data } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  onSuccess: (data) => toast.success(`Hello ${data.name}`), // ← removed in v5
});

# v5 pattern:
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
useEffect(() => {
  if (data) toast.success(`Hello ${data.name}`);
}, [data]);

# Step 3: Update tRPC client setup
# v10:
const client = trpc.createClient({
  links: [httpBatchLink({ url: '/api/trpc' })],
});

# v11 (minimal change — mostly same):
import { createTRPCClient, httpBatchLink } from '@trpc/client';
const client = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: '/api/trpc' })],
});

# Step 4: Check for removed features
# → superjson transformer: still works but moved to explicit serializer
# → The formData procedure type (if you used it) has changed API

# Total migration time:
# Small app: 2-4 hours (mostly React Query v5 changes)
# Medium app: 1 day
# Large app with many queries: 2-3 days (React Query v5 migration dominates)

Should You Use tRPC in 2026?

Use tRPC when:
→ TypeScript monorepo with shared types (the core use case)
→ Full-stack Next.js, SvelteKit, Nuxt, or similar
→ Multiple clients consuming the same API (web + mobile + CLI)
→ Your team values end-to-end type safety over flexibility
→ You're building complex query patterns (search, filters, pagination, subscriptions)
→ You want React Query's caching layer managed automatically

Skip tRPC when:
→ You're building a public API (REST or GraphQL makes more sense)
→ Your team is split between TypeScript and non-TS consumers
→ Simple CRUD forms — Server Actions cover this more simply
→ Micro-services where the "full-stack TypeScript" assumption breaks down
→ Existing REST API you're extending (tRPC works alongside REST, but complicates it)

The alternative landscape (2026):
→ Server Actions: better for mutations, progressive enhancement
→ React Query + Axios: more flexible, less "magic", larger ecosystem
→ GraphQL + codegen: better for complex graph-like data, multi-team APIs
→ REST + Zod: simpler, more explicit, easier to audit

The verdict:
tRPC remains an excellent choice for its target use case.
The v11 upgrade is worth it (React Query v5 is better than v4).
But 2026 has more good alternatives than 2022 did.
Evaluate based on your actual needs, not the hype.

Compare tRPC, GraphQL, and REST-related package trends at PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.