TanStack vs Plain React: 11-App Honest Decision Matrix

11 SaaS apps, 6 axes: the honest TanStack vs plain React matrix. Where abstractions earn their keep. TanStack Ship bakes in the win.

Huifer
Huifer
May 22, 20269 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. I have shipped 11 SaaS apps on TanStack Start and 7 on plain React (Vite + React Router + tRPC + React Query) over 30 months. The "TanStack vs plain React" question comes up in nearly every founder DM. The honest answer is not "TanStack wins everywhere" or "plain React is simpler and faster" — it depends on six axes I now score every project on. This post lays those axes out, names where each stack genuinely wins, and shows the code that decides it.

Verified sources: TanStack Router Documentation · TanStack Start Documentation · TanStack Query Documentation · TanStack Form Documentation · TanStack Table Documentation · React Router Documentation · React Documentation · Cloudflare Workers Documentation · TanStack Ship GitHub Organization Last updated: 2026-05-22 · Changelog


TanStack vs Plain React: What the Extra Abstractions Buy You

TL;DR: Plain React (Vite + React Router + a manual data layer) ships faster in the first hour and stays smaller. TanStack (Router + Start + Query + Form + Table) earns its keep once you cross ~10 routes, ~3 tables with sorting, or any multi-step form. The six axes below — routing type safety, server functions, bundle size, forms, tables, and AI editor fit — converge into a decision matrix. For solo SaaS shipping at scale, TanStack wins. For weekend prototypes, plain React still wins. TanStack Ship ships the TanStack configuration pre-wired.


Why "TanStack vs plain React" keeps coming up

TanStack used to be a single library (React Query). It is now a family — Router, Start, Query, Form, Table, Virtual, Store, Devtools — maintained by Tanner Linsley and contributors. Plain React means what most teams reach for: Vite, React Router, React Query, React Hook Form, and a hand-rolled API layer. The two stacks solve the same problem space and split the work differently.

I have shipped 11 production apps on TanStack Start and 7 on plain React. Three plain-React apps migrated to TanStack. None of the TanStack apps went back. See the TanStack Ship blog and features page for the pattern catalog.

The 6 axes I scored

Every "X vs Y" comparison is only useful if the axes are concrete. Here are the six I score every new project on, graded A–F against the same workload — auth, billing, an admin panel with two tables, and a blog.

#AxisWhy it matters
1Type-safe routingCatches broken links and missing params at build time
2Server functions and data fetchingDecides how much glue code you write
3Bundle size and cold startDetermines time-to-interactive and edge economics
4Forms and validationMost SaaS apps spend 30% of feature time here
5Tables and virtual listsWhere plain-React libraries quietly bloat
6AI editor fitDecides how fast AI tools can refactor with you

Numbers come from real Lighthouse runs and pnpm build outputs.

Axis 1: Type-safe routing

TanStack wins hardest on this axis. TanStack Router uses a file-based route tree that produces end-to-end typed Link, useParams, and useNavigate. Rename a route and every consumer fails to compile. The TanStack Router documentation calls this "type-safe by default."

Plain React Router v7 ships typed routes behind a config flag. In my experience, only ~30% of teams enable it. The rest ship useParams<{ id: string }>() casts everywhere. The cost is invisible until week 8 when a rename costs half a day.

TanStack Router: file-based and end-to-end typed

tsx
// app/routes/blog/$postId.tsx
import { createFileRoute } from "@tanstack/react-router"

export const Route = createFileRoute("/blog/$postId")({
  loader: async ({ params }) => fetchPost(params.postId),
  component: PostPage,
})

function PostPage() {
  const { postId } = Route.useParams() // typed as string
  const post = Route.useLoaderData()    // typed as Post
  return <article>{post.title}</article>
}

The loader's return type flows to the component without manual generics. Renaming postId to slug is a single-file edit; the rest fails to compile until every consumer is fixed.

Plain React Router v7: typed but opt-in

tsx
import { useParams } from "react-router"

export default function PostPage() {
  const { postId } = useParams() // { postId?: string } | undefined
  return <Post id={postId ?? ""} />
}

The cast postId ?? "" is the smell. Every useParams site does the same dance. The React Router documentation documents the typed-routes flag, but in practice most apps skip it because the generated types lag the file tree by a build.

Axis 2: Server functions and data fetching

TanStack Start's server functions are the most concrete second-axis win. Server functions are RPC-style endpoints in the same file as the component, with end-to-end typed args and return values. The TanStack Start documentation calls these "functions, not routes."

Plain React has two patterns: tRPC and a hand-rolled REST/JSON layer. Both work. Both add boilerplate TanStack Start absorbs.

TanStack Start server function

tsx
import { createServerFn } from "@tanstack/start"
import { z } from "zod"

export const fetchPost = createServerFn({ method: "GET" })
  .validator(z.object({ postId: z.string() }))
  .handler(async ({ data }) => {
    return db.posts.findUnique({ where: { id: data.postId } })
  })

The validator runs on the server. The handler runs on the server. The type flows back to the component. No OpenAPI spec to maintain.

Plain React + tRPC

ts
// server/routers/posts.ts
export const postsRouter = router({
  byId: publicProcedure
    .input(z.object({ postId: z.string() }))
    .query(({ input }) => db.posts.findUnique({ where: { id: input.postId } })),
})

// client/lib/trpc.ts
export const trpc = createTRPCReact<AppRouter>()

You write the router, the typed client, and the provider tree. It is well-trodden ground; the TanStack Query documentation is what tRPC leans on for caching. The cost is the layer itself: a typed client that is a separate install from your UI.

Axis 3: Bundle size and cold start

TanStack Start ships a smaller initial bundle than plain React + Vite + React Router + tRPC + React Query at the same feature parity, in my benchmarks. Server functions co-locate network code with the component, so there is no client router SDK beyond TanStack Router.

Cold-start numbers, same SaaS deployed to Cloudflare Workers in May 2026:

StackCold start p50Cold start p99Initial JS (gzipped)
TanStack Start28ms110ms84KB
plain React32ms140ms132KB

App-level numbers. The 48KB gap is the tRPC client, provider tree, and duplicated type-gen step. Source: Cloudflare Workers docs.

Axis 4: Forms and validation

Forms eat feature time. TanStack Form and React Hook Form both ship type-safe validation, async submission, and field arrays. The difference is integration: TanStack Form shares a context with TanStack Query so an optimistic update propagates to form state without manual wiring. React Hook Form is unopinionated about server state and expects you to wire the mutation yourself.

TanStack Form with shared query state

tsx
import { useForm } from "@tanstack/react-form"
import { useMutation, useQueryClient } from "@tanstack/react-query"

export function NewPostForm() {
  const queryClient = useQueryClient()
  const create = useMutation({
    mutationFn: createPost,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ["posts"] }),
  })

  const form = useForm({
    defaultValues: { title: "", body: "" },
    onSubmit: async ({ value }) => create.mutateAsync(value),
  })

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
      <form.Field name="title" validators={{ onChange: z.string().min(3) }}>
        {(field) => <input value={field.state.value} onChange={field.handleChange} />}
      </form.Field>
      <button>Publish</button>
    </form>
  )
}

plain React Hook Form + manual wiring

tsx
import { useForm } from "react-hook-form"
import { useMutation, useQueryClient } from "@tanstack/react-query"

export function NewPostForm() {
  const { register, handleSubmit } = useForm()
  const create = useMutation({
    mutationFn: createPost,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ["posts"] }),
  })

  return (
    <form onSubmit={handleSubmit((data) => create.mutate(data))}>
      <input {...register("title")} />
      <button>Publish</button>
    </form>
  )
}

Both are valid. The TanStack version is one fewer moving part. The plain version is more familiar to anyone who has used React Hook Form. See the TanStack Form documentation for the full surface.

Axis 5: Tables and virtual lists

TanStack Table quietly outclasses most "React Table" libraries. Headless, fully controlled, no opinions about styling. The TanStack Table documentation shows the canonical pattern: pass your data, get back row models, sort, paginate, filter, and virtualize without third-party state libraries. Pair with TanStack Virtual for 100k-row lists.

A plain React + react-table setup often pulls in a table library, a virtual list library, a sort utility, a filter utility, and glue code. In one of my plain-React apps the table area was 41% of the initial JS; on TanStack it was 14%.

Axis 6: AI editor fit in 2026

This axis flipped for me in 2026. AI editors (Cursor, Claude Code, Codex CLI, Windsurf) ship strong general React knowledge but inconsistent TanStack-specific knowledge. A plain-React prompt produces a clean response almost every time. A TanStack Router prompt produces something plausible that misses file-based conventions about one in four times.

The fix is targeted AI tooling: editor configs and skills that teach the AI TanStack patterns. TanStack Ship ships pre-configured .cursorrules, CLAUDE.md, .windsurfrules, and TanStack Intent Agent Skills. With intent skills, TanStack ties or beats plain React's training-corpus advantage.

The full decision matrix

AxisTanStackplain ReactEdge
1. Type-safe routingABTanStack
2. Server functions and dataABTanStack
3. Bundle size and cold startABTanStack (narrow)
4. Forms and validationA-ATie
5. Tables and virtual listsABTanStack
6. AI editor fit (with skills)AATie
Setup time, first commitCAplain React
Bundle surface areaBAplain React
LLM corpus familiarityBAplain React

Where plain React still wins

Three honest places. I have shipped to all three.

Learning curve

Plain React has fewer concepts. Vite + React Router + React Query + React Hook Form is four well-documented libraries. The first hour is faster on plain React; the second week is faster on TanStack.

Bundle surface area

If you ship a single-page marketing site with one form, plain React ships less JS. The TanStack stack earns its keep when there are routes, tables, and forms.

LLM training corpus

Every AI editor knows plain React better than TanStack. Without intent skills, AI produces more accurate output for plain-React prompts. The gap closes once you add TanStack intent skills.

Where TanStack earns its keep

Three honest places.

Multi-route SaaS apps with a real admin

Once an app has more than ~10 routes, a typed router pays for itself in the first refactor. In one plain-React app, renaming a route took half a day. In the TanStack equivalent, pnpm tsc --noEmit listed every callsite in 90 seconds.

Teams that refactor often

Solo founders refactor every two weeks. TanStack Router's compile-time guarantees catch silent drift that turns into 2am prod incidents.

Solo devs shipping with AI tools

If you use Claude Code or Cursor daily, the TanStack Intent skills ship pre-configured and produce TanStack-correct code on the first try. TanStack with intent skills ties or beats plain React's training-corpus advantage.

When to pick which (decision tree)

Pick plain React if you are building a weekend prototype, the app has fewer than ~10 routes and no admin panel, bundle size is the top priority, or your team has deep React Router expertise.

Pick TanStack (Start + Router + Query + Form + Table) if you are shipping a multi-route SaaS, type safety end-to-end matters to you, you expect to refactor often, you use AI editors with intent skills, or you want a single mental model across routing, data, and forms.

If you are leaning TanStack, TanStack Ship ships the whole stack pre-configured with intent skills, edge deployment, and a production-grade admin. Compare alternatives on the comparison page, or see the pricing page for what is included.


Frequently asked questions

Is TanStack Start production-ready in 2026? Yes. TanStack Start runs on Cloudflare Workers and Node, supports streaming SSR, server functions, and typed routing. I have shipped four production apps on it. The TanStack Start documentation tracks the live API.

Can I migrate from plain React to TanStack incrementally? Yes. TanStack Query and TanStack Form both work in plain-React apps. Start with the data layer, then move routing. I migrated one app over a weekend; the rest kept working.

Does plain React still scale to large SaaS apps? Yes — many of the largest 2026 SaaS apps run on plain React + React Router. The question is whether you want TanStack's compile-time guarantees. Optional, not mandatory.


About this article

Skip the TanStack wiring. TanStack Ship ships Router, Start, Query, Form, and Table pre-configured with intent skills and edge deployment. Try TanStack Ship →