Back to all posts

SSR in React: What TanStack Start Actually Changes

Jul 13, 2026
13 min read
SSR in React: What TanStack Start Actually Changes

For most of this series I've drawn a clean line: React runs in the browser, the backend runs on a server, and the contract between them is where the interesting decisions live. That line is useful. It's also about to dissolve.

Server-side rendering is usually sold as a performance trick — render HTML on the server so the first paint is faster and Google can read your page. That's true, and it's the least interesting thing about it. The real change is architectural: SSR takes the wall between "frontend" and "backend" and turns it into a seam inside a single framework. Suddenly some of your React code runs on a server, some runs in a browser, and the hardest part is knowing which is which — and why it matters.

So this article isn't a list of SSR benefits. It's about what actually shifts in how you think when rendering moves to the server, and where TanStack Start lands in a world Next.js has owned for years.

What SSR Actually Buys You

Let me be fair to the standard pitch first, because the benefits are real. Server rendering gives you a faster first contentful paint — the user sees content before a single kilobyte of JavaScript executes. It gives you SEO that actually works, because crawlers get real HTML instead of an empty <div id="root">. And it gives you a better story on slow devices, because the server does the heavy lifting once instead of every phone doing it again.

If that were all SSR did, it'd be a performance optimization you turn on and forget. But turning it on changes the shape of your code, and that's the part nobody warns you about until you hit it.

The Line Moves, and You Have to Know Where

Here's the thing that trips everyone up, and it's worth slowing down on.

In a pure client-side app, all your code runs in one place: the browser. window exists. localStorage exists. Every component runs after the user's device has loaded it. Simple mental model.

The moment you add SSR, your components can run in two environments — once on the server to produce the initial HTML, and again (or partially) in the browser to make it interactive. And the server doesn't have window. It doesn't have localStorage. Code that assumed the browser now runs somewhere the browser's globals don't exist.

function Sidebar() { // Runs on the server first — where 'window' is undefined. // This throws during SSR, and the error is confusing the first time. const collapsed = window.localStorage.getItem('sidebar-collapsed'); return collapsed ? <Collapsed /> : <Expanded />; }

That crashes on the server, and the first time it happens the error message sends you on a goose chase, because it's failing in a place you didn't know your component ran. This is the tax of SSR: you now have to know, for every piece of code, where it executes. Browser-only work — reading localStorage, touching the DOM, using window — has to be deferred to after hydration, usually in an effect. It's not hard once you have the mental model. It's brutal before you do.

With SSR, "where does this run?" stops being a trivia question and becomes the question. Half of SSR debugging is just answering it correctly.

That's the real cost, and no framework fully removes it — it's inherent to running the same code in two places. What a good framework does is make the boundary visible instead of something you discover by crashing.

Hydration: The Weird Middle Step

There's a moment between "server sent HTML" and "app is interactive" that deserves a name, because it's the source of the strangest SSR bugs: hydration.

The server sends fully-formed HTML so the user sees content immediately. But that HTML is inert — no event handlers, no state. Then the JavaScript loads and React "hydrates" it: it walks the server-rendered DOM and attaches all the interactivity, expecting the DOM it finds to match the DOM it would have rendered.

When those two don't match — because you rendered a timestamp, or a random value, or something that read window — you get a hydration mismatch, and React complains. The fix is conceptual, not mechanical: anything that differs between server and client has to wait until after hydration. Once you internalize that server and client must agree on the first render, the whole class of hydration bugs stops being mysterious and starts being obvious.

Where TanStack Start Comes In

Everything so far is true of any SSR framework. So why TanStack Start, in a world where Next.js is the default answer?

Because of what this series has already built. If you've been following along, you're using TanStack Query for server state and TanStack Router for type-safe routing. TanStack Start is a full-stack framework built on that same router — so SSR, server functions, and data loading arrive as a natural extension of tools you already use, not a new paradigm you adopt wholesale.

The piece that matters most is server functions — typed functions you write once and call from the client, where the network boundary between them is handled for you.

import { createServerFn } from '@tanstack/start'; const getPolicy = createServerFn('GET', async (id: number) => { // This body runs ONLY on the server — DB access is safe here. return db.policy.findUnique({ where: { id } }); }); // Called from a component; the type flows across the boundary, // and 'db' never ships to the browser. const policy = await getPolicy(42); // ^? Policy — inferred end to end

If that feels familiar, it should — it's the tRPC idea from the backend article, folded directly into the framework. You write a function that runs on the server, call it from the client with full type safety, and the framework draws the network line for you. The db import never reaches the browser. The type reaches everything. That coherence — one router, one type system, server and client speaking the same language — is the reason Start fits this series so naturally.

The Honest Comparison with Next.js

I'm not going to pretend TanStack Start dethrones Next.js. It doesn't, and picking it over Next.js is a real trade-off, not a free upgrade.

Next.js is mature, enormous, and battle-tested at every scale, with the deepest ecosystem in React and a hosting story that's effectively frictionless on Vercel. Its App Router and React Server Components are a genuinely different, powerful model. If your team wants the safest, most-documented, most-hireable-for choice, Next.js is it, and I'd never argue someone out of it on the merits.

What TanStack Start offers is a different center of gravity. It's router-first and type-obsessed, and if your app is already built on TanStack's pieces, Start is the option where everything agrees — the same router types flow from your routes into your loaders into your server functions with no seams. Next.js asks you to adopt its model of the world; Start extends the model you already chose. For the kind of deeply-typed, client-rich application this series keeps building, that consistency is worth a lot. For a content site that wants RSC and edge rendering out of the box, Next.js is probably still the sharper tool.

That's the honest split: Next.js for maturity and reach, Start for coherence with a TanStack-native stack. Neither is a mistake.

The Real Takeaway

Strip away the framework names and here's what SSR actually asks of you:

  • Know where every piece of code runs. Server or browser is now a question you answer constantly, not never.
  • Defer browser-only work past hydration. window, localStorage, the DOM — none of it exists during the server render.
  • Make server and client agree on the first render. Mismatches are the price of getting this wrong.
  • Let the framework draw the boundary. Server functions turn "which side does this run on" from a guess into a typed contract.

I opened by saying the line between frontend and backend was about to dissolve, and this is what I meant. SSR doesn't move your code to the server — it makes "the server" and "the browser" two places your same code lives, and the skill it demands is holding both in your head at once. That's harder than it sounds and more valuable than it looks. The developers I trust most with a large React app aren't the ones who memorized a framework. They're the ones who always know where their code is running.

With rendering settled, the app needs somewhere to keep its data. The next article is about databases for React developers — the kinds, the trade-offs, and why the modern serverless options change what "add a database" even means.

If you've fought a hydration mismatch that made no sense, or you're weighing Start against Next.js for a real project, I'd like to hear the specifics — those decisions are all about your exact stack, and that's where the interesting part lives.

Found this helpful?

Let's discuss your project needs.

Get in touch