Today's definition of local-thirst is:

Syncing/copying state to the client to drive client-side rendering

Your standard SPA. Let's assume React, but could be Solid, svelte or whatever.

I work on a react-router app. Happens to use tanstack-db and tanstack-query for certain use cases. When I consider the wiring of either it bugs that crap out of me how much is going on under the hood to push some pixels and render something to the DOM.

The browser is a hypermedia rendering engine. It's really great at that, but putting that aside here is what feels like a drag.

Every piece of state or entity I need to add or work with requires definition in multiple places and the plumbing. Assuming you have a database like Postgres:

  • Likely a new table

  • Representation of that data on the server

  • Expose to frontend via API

  • Parse the data on the client

  • Add to normalized store or some cache

Maybe you're using something like https://jazz.tools (really cool project) and you define the entity once, but I don't know if I'd use a sync-engine architecture for a b2b/enterprise ready app. No shade, just calling it out as a caveat to the point I am trying to make about the excess involved when building SPA apps.

Every one of those caries a non-trivial amount of cognitive energy.

So for every new feature/domain you need to go through the ceremony of defining it on the backend then creating some sort of parsable shape that hopefully stays in sync with the backend and you have to reason about only syncing necessary and non-sensitive data. Maybe you can automate it with some sort of code gen, but like… what are we doing here? It feels like the approach inherently invites more code, concepts, complexity, and ceremony. It creates more problems than it solves.

Let's do some hand wavy math.

Say you have an entity post. You'll need a way to CRUD that list, get, create, update, delete. 2 reads, 3 writes. Some of the work is genuinely the domain that is needed for any architecture.

Artifact                     Count
───────────────────────────  ─────
Table + migration                1
Server model / queries           1
Authorization policy             1
Route handler per operation      5
View per screen                 ~2

The rest is all because state cross the network boundary.

Artifact                        Per       Count
──────────────────────────────  ────────  ─────
Response serializer / DTO       endpoint      5
TypeScript wire type            endpoint      5
Client fetch method             endpoint      5
Runtime parse schema            endpoint      5
Query key                       read          2
useQuery wiring                 read          2
Loading / error / empty branch  read          6
useMutation wiring              write         3
invalidateQueries list          write         3
Optimistic update + rollback    write         3
Store collection / normalizer   entity        1
──────────────────────────────  ────────  ─────
Total, per entity                            40

~40 mechanical artifacts for a single entity.

Extrapolating:

Scope                Entities     SPA  Hypermedia
───────────────────  ────────  ──────  ──────────
One new thing               1      40           0
A small feature             4     160           0
Internal admin tool        12     480           0
A product surface          40   1,600           0

Granted it's circumstantial and depends on how you model the domain and what you decide to leave out (heh we can add deleting if someone asks about it later).

I can see justifying it for your core domain. Like at some point your product surface area is bounded and you just deal with it. Where I feel this most is anything adjacent to that. When I go to build something like an admin tool that could benefit from additional entities that support a complex workflow that's internal only. I roll my eyes as soon as I need to add more parsers, more API endpoints that are internal only… the surface area explodes when I just wanted to hook up a friend at work with a cool tool.

Have you ever felt that? Been in that scenario and just decided to build the crappy version that's just a req/res? No realtime or some bolted on polling to approximate it.

Maybe you have the clanker do it all for you and you don't have to worry about it. Did it handle all the ceremony correctly? You're still having to review all that excessive code or at the very least QA and debug any point of failure along the entire data flow. Even if you're vibing all of that you're wasting tokens which will likely become cost prohibitive.

So many desirable properties fall out when you drop SPA and build hypermedia apps with a principled backend. You don't even need to lower the UX ceiling while doing so. Yes there is inherent latency with server round trips, but those can all be masked and mitigated with way simpler techniques compared to syncing the state to the frontend.

You maybe noticed the Hypermedia column in that last table with 0 across the board. None of the surface area explosion is there because you're only implementing the actual domain and necessary complexity. Handlers and templates don't go away (see the first table). Think about how much less you need to make sure is working correctly or Isn't leaking sensitive data. How much less code you or your clanker need to author and maintain. All the concepts you can let fade away like suspense boundaries, use effects, context providers, fine grained reactivity…

That's just the implementation side.

RIP main thread.

Thanks for joining me on part III of my state-of-web-development therapy session.