All field notes

Frontend

Server Components didn't kill the SPA

4 min readBy Arsh Ramgarhia

Two years in, the honest verdict on the App Router: brilliant for content, oversold for dashboards, and the boundary is where all the difficulty lives.

The discourse around React Server Components has been unusually silly, in both directions. One camp says client-side rendering is over. The other says this is all complexity for its own sake. Having now shipped a marketing site, a public portfolio, and a heavily interactive operations console on the App Router, my honest answer is that both camps are describing different applications.

Where they're genuinely excellent

For anything content-shaped, Server Components are the best version of React I've used. Data fetching next to the markup that needs it, no loading spinner waterfall, no client bundle for code that only ever formats a date.

The site you're reading is entirely static. The blog listing, these posts, the project pages — all rendered at build time, no client-side fetching, no state management library. That's the version of the web I actually wanted, and the App Router makes it the default rather than something you have to fight for.

Where the story oversells itself

Now take a dispatch console. Live vehicle positions, a chat panel, filters that need to feel instant, forms with interdependent fields. Almost every component needs interactivity, which means almost every component is a client component, which means you've reinvented the SPA with extra ceremony.

That's fine. It's the correct outcome for that application. What isn't fine is the pressure to feel bad about it, or to contort a genuinely interactive product into a server-first shape because that's the recommended direction.

The framework's default should match your most common page, not your most impressive demo.

The boundary is where the difficulty actually lives

Here's what nobody warns you about clearly enough: the hard part isn't server components or client components. It's the seam between them.

Props crossing that boundary have to be serialisable. Not 'should be' — have to be. So the function you wanted to pass down doesn't go. The class instance doesn't go. The Date works but comes back subtly different than you expected. Context doesn't cross at all, so a provider high in your server tree simply isn't there for the client components underneath it.

  • Put 'use client' as far down the tree as you can bear, not at the top of the page
  • Push providers into a small client wrapper rather than making the whole layout a client component
  • Pass plain data across the boundary — ids and objects, not callbacks and instances
  • Expect one confusing afternoon per project where the answer turns out to be 'that's a boundary problem'

What I actually do now

I stopped picking a side and started picking per route. Marketing pages, blogs, documentation, anything crawlable: server-first, static where possible, ship almost no JavaScript. Consoles, editors, anything where the user lives in the page for an hour: client-heavy, and I don't apologise for it.

The mental model that finally made it click

What unstuck this for me was to stop thinking of 'use client' as a performance directive and start reading it as a boundary marker. It doesn't say 'this is slow' or 'this is interactive'. It says: from here down, this code ships to the browser.

Once I read it that way, the placement rules stopped feeling arbitrary. Of course context doesn't cross — the provider ran on a server that has already finished responding. Of course you can't pass a function — there's nothing on the other side to call it. The constraints aren't React being awkward, they're the constraints of a network boundary, which we've always had. We just used to draw it explicitly with a fetch call.

What it costs you

An honest accounting, because most write-ups skip this part. Debugging is worse — a stack trace that spans both sides is harder to read than either alone. Some libraries still assume they're running in a browser and fail in ways that take a while to diagnose. And onboarding a developer who knows React but not this model takes real time, because their instincts about where state lives are wrong in a way that isn't obvious from the error.

For a content site that's a fine trade. For a small internal tool with three users, honestly, plain client-side React is still a perfectly good answer and I'd stop feeling guilty about it.

The framework is good enough now to let one application be both, which is the genuinely useful development. Not that server rendering won — that you stopped having to choose once for the whole codebase.

ReactNext.jsServer ComponentsArchitecture
Ready when you are

Let's build something worth opening twice.

From a quiet landing page to a full-blown 3D product, every great build starts with one short conversation. Pick a slot — coffee's on me.