This really resonates with something I've been working on. The article nails the core problem: we maintain JavaScript state that's just a shadow copy of what the DOM already knows.
I built a library called stateless that takes this further. Instead of syncing React state with the DOM, it just reads state directly from the DOM using MutationObserver. Your HTML becomes the single source of truth.
// Instead of useState + onChange + value prop dance
const [value, setValue] = useDomValue('#email', 'value');
The hook watches the DOM element and re-renders when it changes. No hydration mismatch bugs. No state sync issues. The DOM is the state.
It's particularly useful for the patterns the article mentions: progressive enhancement where HTML works first, then React enhances. Your form inputs, disclosure widgets, and native elements already hold their state. Why duplicate it?
stateless is part of a broader open source suite: genx for HTML generation, domx for DOM manipulation, all built on the DATAOS architecture, that together form building blocks for a hyper-efficient framework where the DOM isn't an afterthought you sync to, it's the foundation you build on
The mental model shift is weird at first ("wait, I'm not managing state?") but once it clicks, you stop fighting the browser and start letting it do its job.
Haha yeah you are right, that is exactly the thing I was tripping over, I had React state just babysitting the DOM for no real product win.
And your approach is a cool mental flip ""Stop syncing, just read what is already true"" that fits perfectly with the whole HTML first then enhance idea.
So, I have two quick questions though, because this is where I always get nervous:
- First, where do you draw the line between DOM state that is fine to trust and app state that should still live in data, like validation errors, derived values, server driven defaults, stuff that needs to be consistent across SSR and client.
- Second, how does the MutationObserver part behave when things get busy. If a page has lots of inputs or updates, does it stay cheap. Do you scope it per form or per component, or is it one observer with filtering.
Either way the 2KB no deps thing is a flex, dropping a link is fair, people can decide if they want that model. I am going to skim the repo.
Great questions, these are exactly where the mental model matters most.
Where to draw the line? The answer is "further than you'd think." Validation errors? data-error attributes or aria-invalid on the input. Derived values? Compute them from DOM on demand rather than caching. Server-driven defaults? Server renders them INTO the DOM, then DOM is truth.
The key rule of thumb: if it affects what the user sees, it belongs in the DOM. If you're putting state in JS just to render it back to the DOM, you've created a sync problem. If it's data persistance or state that only code cares about... back-end, pure function, using some efficient memory structure.
SSR consistency is actually easier this way. Server renders HTML. Client reads it. No hydration mismatch because there's nothing to reconcile.
MutationObserver performance? Scoped per component, not global. Use subtree: false when you can, attributeFilter to watch only specific attributes. The browser batches mutations automatically (delivered in microtask), so rapid changes don't mean rapid callbacks.
In practice it's cheaper than React's virtual DOM diffing for most UI. You're not diff-comparing object trees, you're getting notified exactly what changed.
Thanks, that explanation is clear. Two things I am still trying to picture in practice.
- For ""derived values from DOM on demand"", what do you do when the derived value is expensive or used in multiple places. Do you just accept recomputing, or do you have a pattern to keep it from turning into lots of repeated DOM reads.
-And for bigger interactions like table row selection, keyboard navigation, drag and drop, does your approach still model that as DOM attributes and queries, or do you keep a small in memory store for that kind of state.
The MutationObserver tips are useful too, scoping and attributeFilter feels like the difference between this being neat and this being a footgun. I will take a look at the repo and the book. Thanks
I used to ship my app with a JavaScript first mindset. It felt fast on my own laptop, but users on weak phones, slow networks, or locked down corporate browsers kept running into sticky pages and half awake UI.
After I finally measured what I was shipping, I realized most of the client code was not adding value. It was just making hydration heavier, breaking accessibility, and hiding simple HTML solutions.
The post walks through the process I used to cut around 80 percent of the JS without going full “no JS”: listing real interactions in human language, leaning on native elements like details and dialog, using bundle analyzers, and deleting dependency creep. It also shows the small performance and accessibility checklist I use now.
This really resonates with something I've been working on. The article nails the core problem: we maintain JavaScript state that's just a shadow copy of what the DOM already knows.
I built a library called stateless that takes this further. Instead of syncing React state with the DOM, it just reads state directly from the DOM using MutationObserver. Your HTML becomes the single source of truth.
The hook watches the DOM element and re-renders when it changes. No hydration mismatch bugs. No state sync issues. The DOM is the state.It's particularly useful for the patterns the article mentions: progressive enhancement where HTML works first, then React enhances. Your form inputs, disclosure widgets, and native elements already hold their state. Why duplicate it?
stateless is part of a broader open source suite: genx for HTML generation, domx for DOM manipulation, all built on the DATAOS architecture, that together form building blocks for a hyper-efficient framework where the DOM isn't an afterthought you sync to, it's the foundation you build on
Zero runtime dependencies, ~2KB gzipped: https://github.com/anthropics/stateless (or wherever it's hosted)
The mental model shift is weird at first ("wait, I'm not managing state?") but once it clicks, you stop fighting the browser and start letting it do its job.
Haha yeah you are right, that is exactly the thing I was tripping over, I had React state just babysitting the DOM for no real product win. And your approach is a cool mental flip ""Stop syncing, just read what is already true"" that fits perfectly with the whole HTML first then enhance idea.
So, I have two quick questions though, because this is where I always get nervous: - First, where do you draw the line between DOM state that is fine to trust and app state that should still live in data, like validation errors, derived values, server driven defaults, stuff that needs to be consistent across SSR and client. - Second, how does the MutationObserver part behave when things get busy. If a page has lots of inputs or updates, does it stay cheap. Do you scope it per form or per component, or is it one observer with filtering.
Either way the 2KB no deps thing is a flex, dropping a link is fair, people can decide if they want that model. I am going to skim the repo.
Great questions, these are exactly where the mental model matters most.
Where to draw the line? The answer is "further than you'd think." Validation errors? data-error attributes or aria-invalid on the input. Derived values? Compute them from DOM on demand rather than caching. Server-driven defaults? Server renders them INTO the DOM, then DOM is truth.
The key rule of thumb: if it affects what the user sees, it belongs in the DOM. If you're putting state in JS just to render it back to the DOM, you've created a sync problem. If it's data persistance or state that only code cares about... back-end, pure function, using some efficient memory structure.
SSR consistency is actually easier this way. Server renders HTML. Client reads it. No hydration mismatch because there's nothing to reconcile.
MutationObserver performance? Scoped per component, not global. Use subtree: false when you can, attributeFilter to watch only specific attributes. The browser batches mutations automatically (delivered in microtask), so rapid changes don't mean rapid callbacks.
In practice it's cheaper than React's virtual DOM diffing for most UI. You're not diff-comparing object trees, you're getting notified exactly what changed.
The DATAOS book (https://dataos.software/book) goes deeper on the architecture if you want the full philosophy.
Thanks, that explanation is clear. Two things I am still trying to picture in practice.
- For ""derived values from DOM on demand"", what do you do when the derived value is expensive or used in multiple places. Do you just accept recomputing, or do you have a pattern to keep it from turning into lots of repeated DOM reads. -And for bigger interactions like table row selection, keyboard navigation, drag and drop, does your approach still model that as DOM attributes and queries, or do you keep a small in memory store for that kind of state.
The MutationObserver tips are useful too, scoping and attributeFilter feels like the difference between this being neat and this being a footgun. I will take a look at the repo and the book. Thanks
I used to ship my app with a JavaScript first mindset. It felt fast on my own laptop, but users on weak phones, slow networks, or locked down corporate browsers kept running into sticky pages and half awake UI.
After I finally measured what I was shipping, I realized most of the client code was not adding value. It was just making hydration heavier, breaking accessibility, and hiding simple HTML solutions.
The post walks through the process I used to cut around 80 percent of the JS without going full “no JS”: listing real interactions in human language, leaning on native elements like details and dialog, using bundle analyzers, and deleting dependency creep. It also shows the small performance and accessibility checklist I use now.