This post is for anyone who has wasted days, weeks, or months of their life struggling with state in your single-page JavaScript app. Maybe you type three paragraphs into an edit field — and a background refresh silently replaces the DOM, vaporizing your work. Or you bring up a modal dialog box, and something makes it disappear. Or your dropdown selection doesn't actually make it into the operation it was intended for.
If this sounds like you, you probably need Datastar in your life, and reduce your use of JavaScript in your client.
I had this realization while listening to a talk that David Yang (@dyang , founder of Fullstack Academy and Lightweight Labs) at Clojure/conj 2025. It made me realize that when you’re writing a JavaScript front-end, you’re almost always accidentally building a distributed system.
Because of David, I ended up deleting ~1,700 lines of JavaScript in a week in an editor I've been writing, replacing it with a radically simpler approach: server-side Clojure + Datastar + SSE (Server-Sent Events). The server renders HTML, streams it to the browser, and the browser just displays it. Hundreds of lines of JS became ~20 lines of Clojure, over and over again.
This is because I realize that the mess I had made for myself (enthusiastically encouraged by Claude Code, trained on a gazillion JS SPA apps) had became intolerable: The client has state. The server has state. They're stomping on each other, and you've lost track on which is actually the source of truth. You fix a bug in one place and it surfaces somewhere else, because you have two copies of reality and they're slowly diverging. And the longer you work on it, the more duct tape you add to keep two diverging copies of truth pointed at each other.
In Yang's talk, he talks about how he thought his main business challenge was to build a sync engine between browser clients and his server to present their QuickBooks data in a Google Sheets like interface to their customers. He kept building more sophisticated solutions to reconcile state: cache invalidation, optimistic updates, conflict resolution, offline queues, replay logic. The code kept getting more complex, and the bugs never went away.
In a moment of reflection and hammock-time, he saw three things in quick succession that led to a startling epiphany:
- "One Billion Checkboxes," the viral app (written in Clojure by @anders_murphy!) serving one billion shared checkboxes to thousands of concurrent users, fully server-rendered on a $5/month VPS. Every session was an HTML stream, with toggles synced in real-time, with zero client-side state management. As Yang said, "I'm trying to render a thousand cells in the browser, and this guy posts about rendering a billion cells in the browser, and you're like: This is relevant to my interests." Haha, so true.
- Watching his son play Fortnite streamed via NVIDIA GeForce NOW, streaming 60fps gameplay with no game engine running on his machine. Like the one billion checkboxes being streamed in each frame, Fortnite is streaming a ton of data to each gamer, and it works. His son's gaming computer was basically a monitor receiving pixels from the network.
- Watching his wife who works at JP Morgan using a virtual desktop, which was streaming a bunch super, complicated enterprise apps as as pixel streams.
Each of these had zero state sync bugs, because there was nothing to sync.
Yang said: "You give someone state they'll have bug for a day but you teach them how to represent state in two places they have to be kept in sync and uh they'll get bugs for a lifetime." Har har. Ouch.
In 2019, I described my Clojure and functional programming aha moment. I proudly talked about how much I learned from reading "React for People Who Know Just Enough jQuery to Get By," I presented the killer toy tweetbox example go from simple to nightmarish — a character counter here, a photo button there, and suddenly jQuery's callback spaghetti was unmanageable.
In contrast, React's centralized state model felt like enlightenment. I wrote my Love Letter to Clojure, and how Clojure eliminated 90% of the errors I used to make. I thought I'd learned the lesson.
I thought there were only two floors of Hell. Apparently there's a third — Claude Code will invariable take you there, and may make sure you never leave. This post is meant to show you the way out.
- Floor 1 (2016): Don't mutate state. Pure functions, immutable data. Stop changing variables out from under yourself. (Clojure and functional programming, lodash, etc.)
- Floor 2 (2019): Centralize state. One atom, one source of truth. Stop scattering state across components, callbacks, and global variables. (jQuery -> React → Redux, Flux, etc.)
- Floor 3 (2025): Don’t duplicate state across client and server — eliminate the client copy entirely if you can . (Electron/JS → server-side Clojure + Server Side Events [SSE]/Datastar)
For the last two weeks, I've been working on a text editor to help me write — it was my sixth attempt to write the ideal editor to help me write. I did a fresh rewrite as Electron JS app, and it worked wildly well, but it was growing unruly.
I rewrote it as a web app, Clojure on the back-end, and JavaScript on the front-end. And started having all sorts of problems, as I described above, due to 50+ shared state across the front- and back-end. Here's some Claude Code summaries of days of bug bashing:
- Ghost state everywhere. Modal says it's closed, but a localStorage restore opens it 500ms later. Dropdown shows Opus, spinner says Sonnet. A boolean set to true on button click is never set to false, so every subsequent update auto-scrolls the page forever. Client state and server state drift apart silently — no errors, just wrong behavior.
- The DOM isn't yours. You set a button to "Loading..." in JavaScript, but the server replaces the DOM before your cleanup runs. You add a purple focus ring, and the next server push erases it. You double-click to edit, but the first click triggers a re-render that replaces the element before the second click arrives. Every piece of state you store in the DOM exists on borrowed time.
- Invisible coordination bugs. Eleven bugs compounding, each masking the next — a scroll position saved by one system and restored by another, cancelling a third system's scroll animation, all within 50ms. You can't reproduce it. You can't even describe it to someone. You just stare at a dropdown that "does nothing."
These were the categories of problems I was drowning in — the app was working, built quickly to during writing sessions, but getting impossible to change without breaking something else:
Armed with Yang's lessons, I started mass-deleting JavaScript and HTMX calls, instead, streaming HTML fragments via SSE (Server-Sent Events) using Datastar. I started adopting the "game-engine" pattern, where the server assembled each frame from scratch, and streams it to the client as HTML. The browser started to become Yang said it should be: a monitor, like his son's Fortnite session.
A couple of amazing surprises: I expected SSE round-trips to feel sluggish, which is usually why we keep things on the client. I was shocked that keyboard navigation could use client-side reactive signals (no network) — it was instant. Skeptically, I tried moving the character counter to be computed on the server, rendered to the client via SSE. To my shock, it was instantaneous.
So I began a big rewrite around three architectural principles:
- Think game engine. The server is a game loop: receive input, mutate state, render frame, push HTML. The browser is a dumb terminal — it displays what it receives and never decides what to show.
- Minimize JS state to zero. I added a red "measles" overlay that highlights every JavaScript state variable on screen. Three variables became an embarrassment. The goal: if the server restarts or the page reloads, everything is restored from one Clojure atom — no localStorage, no JS globals, no let currentState =. (Well, I'm discovering that there's around 5 reason you need to have JS local state, but they're really at the periphery of your program.)
- Fire and forget. Every user action is a POST that returns 204 No Content. No JSON response. No .then(data => {...}). The client fires the request and moves on. The server mutates state and pushes the new reality via SSE.
The best proof of the value of the rewrite: I've had writing sessions where I was actively adding features in the app while using it. Adding keyboard accelerators to buttons when I became annoyed at clicking those buttons. No compile step. No browser refresh. No lost state. The modal stays open, the draft stays in the textarea.
It felt genuinely sublime, even compared to the the world-standard of ClojureScript hot reload systems. Server-side SSE push with browser hot reload feels amazing.
A couple of closing thoughts:
1. If any of these problems sound familiar, watch David Yang's talk. If you're a Clojure person (or Clojure-curious), the combination of Datastar + SSE + server-rendered Hiccup is freaking amazing.
2. If you're vibe coding applications, you're more at risk of falling into this third plane of Hell. It's been trained on so much JavaScript SPA that use the replicated state patterns; you'll have to work extra hard to make sure it uses the "game engine Datastar on backend" pattern. I've created a Claude Code skill for this.
3. I'm still figuring out some of the rough edges of Datastar, as well as things that don't work quite so easily when trying to replace JavaScript and DOM operations. The seven valid uses of JS include:
- Keystroke dispatch. Capture keypress, read server state from data attributes, route it. Must happen in <16ms.
- Cursor manipulation. insertTextAtCursor(), selection ranges, execCommand('insertText'). The browser owns the cursor — SSE can't touch it.
- Browser gestures. Clipboard API, drag-and-drop, double-click. Requires user gesture or instant feedback that can't survive a round-trip.
- Heartbeat/timers. SSE connection monitor, loading spinners, MutationObserver for post-morph cleanup. Detecting absence is inherently client-side.
- Lifecycle interception. beforeunload beacon, deferring reload during editing. Only the browser can intercept the browser.
I hope this helps someone who is suffering.
@renault_uk disappointing that Windows or Mac (American tech) it is a requirement, to be able to update the map in the Clios navigation system.
Linux have been able to write to USB disks / dongles since forever.
Live coding a full game level using only the REPL. No restarts, no reloads, just pure flow.
This is how I build levels while the game is running.
If you like game dev, Lisp, or live workflows, this is for you 👇
How to set up a local AI environment using Ollama and OpenWebUI on Docker, including tips for writing an effective system prompt.
https://t.co/iPNn7viaU1