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 ~2The 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 0Granted 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.