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.
| # | Axis | Why it matters |
|---|---|---|
| 1 | Type-safe routing | Catches broken links and missing params at build time |
| 2 | Server functions and data fetching | Decides how much glue code you write |
| 3 | Bundle size and cold start | Determines time-to-interactive and edge economics |
| 4 | Forms and validation | Most SaaS apps spend 30% of feature time here |
| 5 | Tables and virtual lists | Where plain-React libraries quietly bloat |
| 6 | AI editor fit | Decides 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
// 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
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
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
// 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:
| Stack | Cold start p50 | Cold start p99 | Initial JS (gzipped) |
|---|---|---|---|
| TanStack Start | 28ms | 110ms | 84KB |
| plain React | 32ms | 140ms | 132KB |
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
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
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
| Axis | TanStack | plain React | Edge |
|---|---|---|---|
| 1. Type-safe routing | A | B | TanStack |
| 2. Server functions and data | A | B | TanStack |
| 3. Bundle size and cold start | A | B | TanStack (narrow) |
| 4. Forms and validation | A- | A | Tie |
| 5. Tables and virtual lists | A | B | TanStack |
| 6. AI editor fit (with skills) | A | A | Tie |
| Setup time, first commit | C | A | plain React |
| Bundle surface area | B | A | plain React |
| LLM corpus familiarity | B | A | plain 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
- Written by Huifer, solo developer and maintainer of TanStack Ship. Shipped 11 SaaS apps on TanStack Start and 7 on plain React over 30 months.
- Verified sources: TanStack Router · TanStack Start · TanStack Query · TanStack Form · TanStack Table · React Router · React · Cloudflare Workers · TanStack Ship GitHub
- Last updated: 2026-05-22 · Changelog
Skip the TanStack wiring. TanStack Ship ships Router, Start, Query, Form, and Table pre-configured with intent skills and edge deployment. Try TanStack Ship →