<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matteo Antony Mistretta</title>
    <description>The latest articles on DEV Community by Matteo Antony Mistretta (@iceonfire).</description>
    <link>https://dev.to/iceonfire</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3571361%2F4a5d3fb6-78a4-4644-a2b8-f690f41d7cb6.jpg</url>
      <title>DEV Community: Matteo Antony Mistretta</title>
      <link>https://dev.to/iceonfire</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iceonfire"/>
    <language>en</language>
    <item>
      <title>Why AI Hates Modern Frameworks (and Loves Web Standards)</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 30 Jun 2026 12:30:16 +0000</pubDate>
      <link>https://dev.to/iceonfire/why-ai-hates-modern-frameworks-and-loves-web-standards-jgl</link>
      <guid>https://dev.to/iceonfire/why-ai-hates-modern-frameworks-and-loves-web-standards-jgl</guid>
      <description>&lt;p&gt;There's a paradox nobody wants to say out loud: the same frameworks companies pick because they're "enterprise-ready," "scalable," and "industry standard" are, for an LLM writing code, a minefield.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://angular.dev/" rel="noopener noreferrer"&gt;Angular&lt;/a&gt;, &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt; with its whole ecosystem, &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;Nx&lt;/a&gt; with its monorepos: these are powerful tools, built by humans to coordinate teams of humans on massive codebases. And for that purpose, they're often the right choice — if your primary constraint is coordinating hundreds of engineers over a decade, the conventions and tooling of an established framework earn their keep.&lt;/p&gt;

&lt;p&gt;But there's a second actor in the room now. When the one writing the code is an AI, the very traits that make these frameworks "robust" turn into pure friction. The argument I'm making isn't "Angular and React are obsolete." It's narrower: we've historically optimized software architecture for human cognition, and LLMs introduce a different cost model that may favor simpler, more deterministic architectures — at least in some domains. Let's break down why, in three points.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Token Tax (and the Cognitive Bottleneck)
&lt;/h2&gt;

&lt;p&gt;An LLM doesn't "understand" code the way we do — it processes it token by token, and every token costs something: money, latency, and context window that could otherwise be spent reasoning about the actual problem.&lt;/p&gt;

&lt;p&gt;Try asking an AI to generate a simple input form in a typical Angular/Nx context. To do it "properly" it has to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create the component (separate &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.html&lt;/code&gt;, &lt;code&gt;.css&lt;/code&gt; files)&lt;/li&gt;
&lt;li&gt;declare the &lt;code&gt;@Component&lt;/code&gt; with all its metadata&lt;/li&gt;
&lt;li&gt;import and wire up the right modules&lt;/li&gt;
&lt;li&gt;possibly touch an &lt;code&gt;NgModule&lt;/code&gt; or a standalone-components config&lt;/li&gt;
&lt;li&gt;navigate 4-5 folder levels inside a typical Nx structure (&lt;code&gt;apps/&lt;/code&gt;, &lt;code&gt;libs/&lt;/code&gt;, &lt;code&gt;feature-x/&lt;/code&gt;, &lt;code&gt;data-access/&lt;/code&gt;, &lt;code&gt;ui/&lt;/code&gt;...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this &lt;em&gt;before&lt;/em&gt; writing a single line of actual logic. That's architectural complexity that, for a human, pays for itself over time thanks to tooling, autocomplete, and internalized conventions. For an LLM generating text sequentially, it's a tax paid on every single request.&lt;/p&gt;

&lt;p&gt;My thesis: &lt;strong&gt;modern architectural complexity isn't just a cognitive cost for developers — it's a direct economic and latency tax on AI.&lt;/strong&gt; More files, more boilerplate, more layers of indirection means more tokens, which means more time and more cost for every single generative interaction. Yes, context windows are growing and agents are getting smarter at navigating large repositories. But fewer tokens is still fewer tokens, at any scale — cheaper and faster doesn't stop being an advantage just because the baseline improves.&lt;/p&gt;

&lt;p&gt;The alternative is a flat architecture, built on native web standards, where logic is deterministic and localized. In Inglorious Web, for instance, a component is an object with handlers and a &lt;code&gt;render&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ContactForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;input @input=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#contactForm:fieldChange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
      &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt; /&amp;gt;
    `&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No decorators, no modules to register, no folders to traverse. One file, one unit of meaning. For an LLM, that translates into a drastically smaller generative context — and in practice, a near-instant response instead of a whole reasoning pass spent figuring out where and how each piece fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"But what about &lt;a href="https://svelte.dev/" rel="noopener noreferrer"&gt;Svelte&lt;/a&gt; or &lt;a href="https://www.solidjs.com/" rel="noopener noreferrer"&gt;Solid&lt;/a&gt;?"&lt;/strong&gt; — fair question, and the one a skeptical reader is probably already forming. Yes, both are lighter than Angular or React. But they don't fully solve the problem, for one fundamental reason: they both require a build step. Svelte compiles its template syntax into vanilla JavaScript before anything reaches the browser. Yes, human developers also reason about Svelte semantics rather than generated output — but they rely on compiler guarantees that have been tested and documented over time. An LLM has fewer opportunities to verify that the source it generates behaves as intended once that transformation step is applied. It introduces another semantic layer between what the model writes and what the runtime ultimately executes. SolidJS is closer to the metal, but its fine-grained reactivity — signals that automatically track dependencies at runtime — introduces a different kind of &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;implicit magic&lt;/a&gt;: the LLM has to predict what the runtime will track, not just what the code says. These aren't fatal problems, but they are additional sources of unpredictability that a no-build, standards-native approach simply doesn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Boilerplate-Induced Hallucinations
&lt;/h2&gt;

&lt;p&gt;The more speculative code a codebase has — patterns added "just in case," preemptive abstraction layers, optional configurations — the more surface area an LLM has to get confused on.&lt;/p&gt;

&lt;p&gt;If an AI has to navigate Signals, RxJS, a couple of custom stores, and maybe a different state-management library per feature (the jungle you've probably already run into in some Angular or React project that grew badly), it has more opportunities to hallucinate. It'll invent a method that doesn't exist, mix two incompatible patterns, break the app on the first refactor because it "guessed" a convention that wasn't actually the one the project used.&lt;/p&gt;

&lt;p&gt;Generative AI excels when working close to platform-native abstractions: HTML, CSS, modern JavaScript. That's where it has seen the most examples, the most consistency, the least variance from one project to another. Every architectural "house of cards" we build on top of the native platform is one more dialect the model has to learn to recognize — and statistically, one it will get wrong more often.&lt;/p&gt;

&lt;p&gt;A framework with &lt;strong&gt;a single state pattern&lt;/strong&gt; (everything lives in the entity, never in component closures) and &lt;strong&gt;a single rule for changing it&lt;/strong&gt; (&lt;code&gt;api.notify()&lt;/code&gt;, always) eliminates entire branches of ambiguity. There's no choosing between &lt;code&gt;useState&lt;/code&gt;, a signal, or a Redux selector: there's one way, always the same way. Fewer arbitrary choices to make means fewer chances to get it wrong.&lt;/p&gt;

&lt;p&gt;And when something &lt;em&gt;does&lt;/em&gt; go wrong — because it will — you're not left debugging magic. &lt;a href="https://inglorious.dev/web" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt; is built on the &lt;a href="https://redux.js.org/understanding/thinking-in-redux/three-principles" rel="noopener noreferrer"&gt;three core principles of Redux&lt;/a&gt; (single source of truth, read-only state, changes through pure functions) and is fully compatible with &lt;a href="https://github.com/reduxjs/redux-devtools" rel="noopener noreferrer"&gt;Redux DevTools&lt;/a&gt;. This means you get time-travel debugging out of the box: you can replay every state transition, inspect exactly what changed and when, and pinpoint the moment the AI-generated code produced an unexpected result. The same determinism that makes the architecture legible to an LLM makes it legible to a developer with DevTools open. If you want to go deeper on this, I've covered the Redux principles and how Inglorious Web applies them in &lt;a href="https://dev.to/iceonfire/entity-centric-architecture-a-different-way-to-think-about-web-uis-4mkd"&gt;earlier articles&lt;/a&gt; — but the short version is: predictable state flow is a property that benefits both the model generating code and the human debugging it afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "Just-In-Time UI" Is Already a Real Use Case — Even If It Stays Niche
&lt;/h2&gt;

&lt;p&gt;I'm not going to claim that the 400-screen enterprise app is dead, or that users are about to stop wanting the button to always be in the same place. Consistency, auditability, accessibility — these are real constraints that keep pre-built UIs relevant, especially in regulated industries. Generative UI may never become the dominant paradigm, and it would be naive to predict otherwise.&lt;/p&gt;

&lt;p&gt;But here's the thing: it doesn't need to be dominant to expose a genuine limitation in today's frameworks.&lt;/p&gt;

&lt;p&gt;There are already real use cases where generating UI components on demand makes sense — data exploration tools, internal dashboards, custom reports. In these contexts, an AI receives a dataset and produces the visual layer needed to interact with it, just for that moment. Niche? Probably, yes. Nonexistent? Absolutely not.&lt;/p&gt;

&lt;p&gt;And for this to work at all — even in a narrow niche — the rendering has to be fast. To be fair, both React and Angular have evolved significantly here: server components, partial hydration, and streaming have addressed many of the performance complaints that were valid a few years ago. But even in their modern form, they carry runtime overhead and initialization costs that are hard to shed entirely — because they were designed for complex, stateful applications, not for components assembled on the fly and discarded moments later.&lt;/p&gt;

&lt;p&gt;What on-demand UI requires is a rendering engine with no intermediate compilers and no heavy runtime to initialize: re-render the whole tree on every state change and let a library like &lt;a href="https://lit.dev/docs/libraries/standalone-templates/" rel="noopener noreferrer"&gt;&lt;code&gt;lit-html&lt;/code&gt;&lt;/a&gt; handle template reconciliation — efficiently updating only the DOM nodes that actually changed, without a virtual DOM layer in between. No zone.js to wake up, no signal graph to trace. Just: state changes → re-render → the browser applies the diff.&lt;/p&gt;

&lt;p&gt;Whether this pattern will expand beyond its current niche is genuinely unknown. But the frameworks that make it technically impossible aren't even in the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I Designed Inglorious Web
&lt;/h2&gt;

&lt;p&gt;I'm not going to tell you "so go use my framework." But there's one thought I want to leave you with.&lt;/p&gt;

&lt;p&gt;When I designed Inglorious Web, I did it to get back to the simplicity of web standards. Today I realize that same simplicity isn't just there to keep us humans sane — it's the only way to let AI generate interfaces that are fast, cheap, and stable.&lt;/p&gt;

&lt;p&gt;The software of the future may end up looking surprisingly lightweight — not because humans demanded it, but because LLMs reward it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Code AI Won't Write</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:30:05 +0000</pubDate>
      <link>https://dev.to/iceonfire/the-code-ai-wont-write-1ieb</link>
      <guid>https://dev.to/iceonfire/the-code-ai-wont-write-1ieb</guid>
      <description>&lt;p&gt;I use a form validation problem as a technical interview question. It's deceptively simple — and the solutions people reach for reveal a lot about how they think.&lt;/p&gt;

&lt;p&gt;Then I tried it on Claude, ChatGPT, and Gemini. The results were illuminating, but not for the reasons I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Many form libraries share a common convention: form data is represented as a plain nested object, and the validation function returns an object of the same shape containing the errors. You'll find this pattern in &lt;a href="https://formik.org/docs/guides/validation" rel="noopener noreferrer"&gt;Formik&lt;/a&gt; and &lt;a href="https://final-form.org/docs/react-final-form/examples/record-level-validation" rel="noopener noreferrer"&gt;React Final Form&lt;/a&gt; in React, and — full disclosure — in &lt;a href="https://inglorious.dev/web/featured/form.html#validation" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt;, my own framework, which ships form handling built in without any extra dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VR Visor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;homeAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Long St&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;00666&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Short St&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;00777&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;co&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inglorious Coderz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;billingAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Wide Plaza&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;00888&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1142042&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validation function should return an object containing all errors found. A starting example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ask: &lt;em&gt;extend this to validate every field&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Notice that the three address types aren't identical. &lt;code&gt;shippingAddress&lt;/code&gt; requires a &lt;code&gt;co&lt;/code&gt; field. &lt;code&gt;billingAddress&lt;/code&gt; requires a &lt;code&gt;vat&lt;/code&gt;. These differences matter — and how you handle them reveals a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Solutions, Four Instincts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Flag — the average human
&lt;/h3&gt;

&lt;p&gt;The most common approach I see in interviews is a single &lt;code&gt;validateAddress&lt;/code&gt; function with a &lt;code&gt;type&lt;/code&gt; parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;street&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;street&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shipping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;co&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;co&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. But every new address type, every new special rule, becomes another branch inside the same function. The differences between types are hidden rather than expressed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Recursion without a schema — the clever human (and the AIs' first attempt)
&lt;/h3&gt;

&lt;p&gt;The cleverest answer I ever received in an interview was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nestedErrors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nestedErrors&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nestedErrors&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;validateNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No schema, no flags — just a recursive walk of the data shape itself. Elegant, clever, a bit too complex for my taste maybe, but genuinely impressive.&lt;/p&gt;

&lt;p&gt;But it has a silent flaw: it only validates fields that are &lt;em&gt;present&lt;/em&gt; in &lt;code&gt;values&lt;/code&gt;. If &lt;code&gt;billingAddress&lt;/code&gt; arrives without a &lt;code&gt;vat&lt;/code&gt; key entirely, the error is never registered. The function doesn't know what it doesn't see.&lt;/p&gt;

&lt;p&gt;To be clear: the problem here isn't recursion itself. It's that the implementation has no source of truth about what fields are &lt;em&gt;expected&lt;/em&gt;. A recursive validator can work perfectly — if it's driven by something that knows the shape of the domain.&lt;/p&gt;

&lt;p&gt;Interestingly, this was also the first instinct of all three AIs — Claude, ChatGPT, and Gemini independently reached for this same structure. When I pointed out the missing-key problem, they all reached for the same fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Schema + recursion — the AI that wants to cover everything
&lt;/h3&gt;

&lt;p&gt;Once the missing-key flaw is on the table, the natural repair is to drive the iteration from a schema rather than from the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;homeAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;co&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;billingAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nestedErrors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nestedErrors&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nestedErrors&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;validateNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is technically sound. It handles missing keys, it scales, it's data-driven. All three AIs converged on this pattern independently — and honestly, it's not wrong. If address types were configured at runtime via metadata rather than hardcoded in the codebase, this approach would be the right one. When variability lives in data, a data-driven solution wins.&lt;/p&gt;

&lt;p&gt;But here, the schema is just a mirror of the data shape, with &lt;code&gt;true&lt;/code&gt; where values should be. It's a solution to a problem — missing keys — that the composition approach never has in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Function composition — knowing the right tool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;homeAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shippingAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateShippingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shippingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billingAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateBillingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;street&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;street&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateShippingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;co&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;co&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateBillingAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No schema. No recursion. No flags. Each address type is its own function, and the shared logic is composed in.&lt;/p&gt;

&lt;p&gt;The missing-key problem doesn't exist here — &lt;code&gt;validateBillingAddress&lt;/code&gt; always checks for &lt;code&gt;vat&lt;/code&gt;, regardless of what arrives. But the reason isn't just simplicity: it's that &lt;code&gt;validateBillingAddress&lt;/code&gt; exists because a billing address is a &lt;em&gt;distinct business concept&lt;/em&gt;, not because it saves lines of code. The structure of the code reflects the structure of the domain.&lt;/p&gt;

&lt;p&gt;When a new address type appears, you introduce a new validator rather than extending a growing set of conditional rules. Modification doesn't disappear — it becomes localized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Is This the Hardest Solution to Find?
&lt;/h2&gt;

&lt;p&gt;The flag, the recursion, and the schema all share the same instinct: &lt;em&gt;find the common pattern and centralize the logic&lt;/em&gt;. It's what we're taught. It's what most production code looks like. It's a sound instinct — and sometimes the right one.&lt;/p&gt;

&lt;p&gt;The composition approach asks a different question first: &lt;em&gt;where does the variability actually live?&lt;/em&gt; If address types are stable domain concepts that belong in code, then composition wins — each type gets its own function, and differences are expressed rather than parameterized. If address types are dynamic, configurable, and driven by external data, then the schema wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem with the flag, the recursion, and the schema is not that they're wrong — it's that they all assume the answer to that question without asking it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's striking is that the cleverest human in the room and three AI models all converged on the same solution independently. In hindsight, this isn't surprising. Programming education spends years teaching us to eliminate duplication and generalize patterns. Large language models are trained on the output of that culture, so they inherit many of the same instincts. This isn't a story about what AI can't do — it's a reflection on what software engineering culture taught both AI and us to reach for by default.&lt;/p&gt;

&lt;p&gt;AI can write the composition solution. It just rarely chooses to.&lt;/p&gt;

&lt;p&gt;The simplest solution isn't the one that requires the least typing. It's the one that most honestly reflects what the domain is actually telling you.&lt;/p&gt;

&lt;p&gt;Next time you find yourself reaching for a flag or a schema, ask: &lt;em&gt;is this variability in my code, or in my data?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer changes everything.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>interview</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Blaming Re-renders. You're Optimizing the Wrong Thing.</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 09 Jun 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/iceonfire/stop-blaming-re-renders-youre-optimizing-the-wrong-thing-2n8p</link>
      <guid>https://dev.to/iceonfire/stop-blaming-re-renders-youre-optimizing-the-wrong-thing-2n8p</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of a series about &lt;a href="https://inglorious.dev/web" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt;. &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;Start here&lt;/a&gt; if you haven't already, or &lt;a href="https://dev.to/iceonfire/you-just-need-entities-that-can-die-4l80"&gt;go back one post&lt;/a&gt; to see why global state is pretty much always acceptable.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;After &lt;a href="https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g"&gt;the benchmarks post&lt;/a&gt;, I got a question that comes up every time I talk about Inglorious Web's rendering model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If the whole tree re-renders on every state change, wouldn't it be better if you memoized render functions the way React does with &lt;code&gt;React.memo&lt;/code&gt;?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a reasonable question. It's also solving the wrong problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  What memoization actually costs
&lt;/h2&gt;

&lt;p&gt;Memoization isn't free. A cache lookup happens on every call — whether or not the cached value is stale. In a busy UI, that's constant overhead, paid regardless of whether the memo ever saves you anything.&lt;/p&gt;

&lt;p&gt;And it saves you less than you'd think, because lit-html already does its own caching at the template level. It tracks which parts of a template changed between renders and only patches those DOM nodes. Adding a JS-side memo layer on top means you're diffing twice: once in your cache, then again in lit-html. The second diff doesn't know what the first already handled.&lt;/p&gt;

&lt;p&gt;So memoizing render functions in Inglorious Web would mean: always paying a cache lookup cost, sometimes saving a function call, while lit-html does its own DOM diffing regardless. The math doesn't work out.&lt;/p&gt;




&lt;h2&gt;
  
  
  What signals actually cost
&lt;/h2&gt;

&lt;p&gt;The usual counter-proposal is: "fine, skip memoization, but use signals instead. Only update the parts of the tree that depend on what changed."&lt;/p&gt;

&lt;p&gt;Signals are magic. Not in the fun sense — in the sense described in &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;the first post of this series&lt;/a&gt;. Each reactive primitive maintains its own list of subscribers, built and updated at runtime, invisible to you as you write code. When a signal changes, it pushes to that list. Fine-grained reactivity works by having many small signals, each with its own observer registry, each encoding a piece of a dependency graph you didn't write and can't read.&lt;/p&gt;

&lt;p&gt;That's memory. Multiple signals means multiple registries. And those registries encode a dependency graph that's implicit, assembled at runtime, and opaque by design. You can't grep for it, you can't draw it statically, and when it behaves unexpectedly — a component updates that shouldn't, or doesn't when it should — you're debugging something that doesn't exist as readable code anywhere.&lt;/p&gt;

&lt;p&gt;In Inglorious Web, the only observer of state is the root render function. One observer, no registries, no hidden graph.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual benchmark
&lt;/h2&gt;

&lt;p&gt;I've measured the worst case for full-tree rendering: &lt;a href="https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g#benchmark-3-the-deep-tree"&gt;a tree 8 levels deep, 3 children per node, one random node firing an event 3 times per second, over 10 seconds&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Compared to React, Vue, and Svelte — which all use different levels of fine-grained reactivity to update only the affected node — Inglorious Web walks the whole tree every time.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;~100ms of extra scripting time over 10 seconds.&lt;/strong&gt; 1% of wall-clock time, on a workload specifically designed to be as punishing as possible for full-tree rendering. All four frameworks hold 120 FPS throughout.&lt;/p&gt;

&lt;p&gt;The profiler can measure it. No user would notice it. And it comes with a tradeoff in the other direction: Inglorious Web's JS heap runs substantially lower than Svelte's in this scenario. Full-tree rendering trades a small, bounded CPU cost for lower memory pressure and no observer registries to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The right distinction
&lt;/h2&gt;

&lt;p&gt;Here's the thing that often gets missed: the concern about "wasting renders" is almost never really about render functions being called. It's about expensive work happening &lt;em&gt;inside&lt;/em&gt; render functions.&lt;/p&gt;

&lt;p&gt;Those are different problems with different solutions.&lt;/p&gt;

&lt;p&gt;If your render function runs something expensive — a sort, a filter, a derived calculation — that work should be lifted out into &lt;code&gt;compute()&lt;/code&gt;. Not because it makes the render faster to call, but because that computation doesn't belong in a render function in the first place. Render functions should describe structure. Structure is cheap.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;compute()&lt;/code&gt; works exactly like Redux's &lt;code&gt;createSelector&lt;/code&gt;: you pass it an array of input selectors, and a function that receives their results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inglorious/web&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;visibleItems&lt;/code&gt; is now a memoized selector. It only reruns when &lt;code&gt;items&lt;/code&gt; or &lt;code&gt;filter&lt;/code&gt; actually change. Everything else — every render caused by unrelated state changes — skips the computation entirely and returns the cached result.&lt;/p&gt;

&lt;p&gt;You use it in &lt;code&gt;render&lt;/code&gt; like any other selector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TaskList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;visibleItems&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;ul&amp;gt;
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;/ul&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;compute()&lt;/code&gt; memoizes the calculation, not the render. The render function stays a pure, cheap description of structure. lit-html handles the DOM delta. Everyone does their job.&lt;/p&gt;

&lt;p&gt;Memoizing the render function itself would be treating the symptom — "this function is called too often" — rather than the cause — "this function is doing too much." And it would do so at the cost of a cache layer that's always present, always paying its overhead, for a benefit that only materializes in specific circumstances lit-html already handles at a lower level.&lt;/p&gt;




&lt;h2&gt;
  
  
  The caching tradeoff, plainly stated
&lt;/h2&gt;

&lt;p&gt;Signals waste memory to save CPU time. Full-tree rendering wastes a little CPU time to save memory. Both are fine choices. Neither is the absence of a tradeoff.&lt;/p&gt;

&lt;p&gt;The difference is that full-tree rendering is explicit and measurable. You can run the benchmark. You can read the profiler output. You know exactly what you're trading and why.&lt;/p&gt;

&lt;p&gt;A hidden dependency graph built at runtime by a reactive system is harder to audit. It's magic in exactly the sense &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;this series has been pushing against&lt;/a&gt;: invisible machinery doing things on your behalf, breaking in ways you didn't anticipate, in places you didn't author.&lt;/p&gt;

&lt;p&gt;I'll take the 100ms.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>You Just Need Entities That Can Die</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 07 Apr 2026 13:18:44 +0000</pubDate>
      <link>https://dev.to/iceonfire/you-just-need-entities-that-can-die-4l80</link>
      <guid>https://dev.to/iceonfire/you-just-need-entities-that-can-die-4l80</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of a &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;series about Inglorious Web&lt;/a&gt;. It stands alone, but the examples make more sense if you've read the &lt;a href="https://dev.to/iceonfire/entity-centric-architecture-a-different-way-to-think-about-web-uis-4mkd"&gt;architecture post&lt;/a&gt; first, and maybe the &lt;a href="https://dev.to/iceonfire/theres-no-such-thing-as-local-state-2i4j"&gt;post about why it's better to stick with global state.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Forms are one of those relevant cases where the &lt;a href="https://redux.js.org/understanding/thinking-in-redux/three-principles#single-source-of-truth" rel="noopener noreferrer"&gt;Single Source of Truth&lt;/a&gt; principle goes to die.&lt;/p&gt;

&lt;p&gt;Not because the principle is wrong — it isn't. But because forms are volatile. They change on every keystroke. They're submitted and discarded. They have validation state, touched state, dirty state, error state — a whole parallel universe of UI concerns that has nothing to do with your application's domain state. Putting all of that in a Redux store felt principled at the time and turned out to be a mistake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://redux-form.com/" rel="noopener noreferrer"&gt;Redux-form&lt;/a&gt; was that mistake, made by &lt;a href="https://github.com/erikras" rel="noopener noreferrer"&gt;Erik Rasmussen&lt;/a&gt; — enthusiastically, alongside the rest of us who were in love with Redux at the time. The redux-form homepage now reads: "Do not begin a project with Redux Form." Rasmussen learned the same lesson the rest of us did, and built &lt;a href="https://final-form.org/react" rel="noopener noreferrer"&gt;React Final Form&lt;/a&gt; instead. The lesson it taught the ecosystem shaped every form library that followed.&lt;/p&gt;

&lt;p&gt;The real axis here isn't &lt;em&gt;global vs local&lt;/em&gt;. It's &lt;strong&gt;alive vs dead&lt;/strong&gt;. Form state can be global — accessible, debuggable, consistent with the rest of your application state. But it should only exist for as long as the form exists. The problem with redux-form wasn't putting form state in the store. It was never taking it out.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://redux-form.com/" rel="noopener noreferrer"&gt;Redux-form&lt;/a&gt;, you reset forms. In &lt;a href="https://formik.org/" rel="noopener noreferrer"&gt;Formik&lt;/a&gt;, you isolate them. In &lt;a href="https://inglorious.dev/web/featured/form.html" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt;, you destroy them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redux-Form: When the Principle Became the Problem
&lt;/h2&gt;

&lt;p&gt;The idea was coherent: if all state is global and managed by Redux, form state should be too. Redux-form put your form's field values, validation errors, touched fields, and submission state all into the Redux store.&lt;/p&gt;

&lt;p&gt;The problems were practical. Form state that entered the store on mount stayed there after submission — bloating the state tree with stale data from forms the user had already completed. Worse, if you submitted a form and opened it again, you'd retrieve the previous submission's values by default unless you explicitly reset. The store was supposed to be the single source of truth, but the truth it was telling was wrong.&lt;/p&gt;

&lt;p&gt;And then there was performance. Every keystroke in a form field dispatched an action to the Redux store. Every action triggered a re-render of every connected component. For a long form with many fields, typing felt sluggish.&lt;/p&gt;




&lt;h2&gt;
  
  
  Formik and React Final Form: Escaping the Store
&lt;/h2&gt;

&lt;p&gt;The ecosystem's answer was to accept that form state doesn't belong in Redux. Formik managed form state locally in a React component using &lt;code&gt;useRef&lt;/code&gt; and &lt;code&gt;useState&lt;/code&gt; internally, keeping Redux out of it entirely. Clean, practical, and widely adopted.&lt;/p&gt;

&lt;p&gt;React Final Form — also by Erik Rasmussen, who had learned from redux-form — took a similar approach but with a more sophisticated subscription model. Instead of re-rendering the entire form on every change, it let individual fields subscribe to only the state they needed. The clever part was &lt;a href="https://final-form.org/docs/react-final-form/api/FormSpy" rel="noopener noreferrer"&gt;&lt;code&gt;FormSpy&lt;/code&gt;&lt;/a&gt;: a component that could subscribe to specific slices of form state and re-render independently, minimizing the rendering surface area.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FormSpy&lt;/span&gt; &lt;span class="na"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;FormSpy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FormSpy&lt;/code&gt; is genuinely clever engineering. It exists because React's rendering model means that any state change in a parent re-renders all children — so you need an explicit mechanism to opt out of those re-renders for performance. The solution is a subscription system layered on top of React's component model.&lt;/p&gt;

&lt;p&gt;Both libraries solved the redux-form problems correctly. But notice what they're solving: performance issues and stale state problems that arise from a specific architectural context — React's component-centric rendering model and the mismatch between volatile form state and persistent application state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inglorious Web: Forms as Entities
&lt;/h2&gt;

&lt;p&gt;In Inglorious Web, a form is an entity. You compose the built-in &lt;code&gt;Form&lt;/code&gt; primitive with your own type, add a &lt;code&gt;submit&lt;/code&gt; handler, and declare the entity in the store with its initial values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Form&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inglorious/web/form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inglorious/web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ContactForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contactFormSubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remove&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// optional — destroy the entity on submit&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;form @submit=&lt;/span&gt;&lt;span class="p"&gt;${()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:submit`&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;
        &amp;lt;input
          .value=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
          @input=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:fieldChange`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;
        /&amp;gt;
        &amp;lt;input
          .value=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
          @input=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:fieldChange`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="s2"&gt;
        /&amp;gt;
        &amp;lt;button type="submit"&amp;gt;Send&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    `&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ContactForm&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;autoCreateEntities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The form's field values, validation state, and touched state all live in the store as part of the entity. When the user submits, you handle it in the &lt;code&gt;submit&lt;/code&gt; handler. And if you want the form to disappear after submission — for a modal form, a one-time wizard step, a transient data entry panel — you notify &lt;code&gt;remove&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;remove&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The entity is destroyed, its state is removed from the store, and because &lt;code&gt;render&lt;/code&gt; is bound to the entity, the form disappears from the UI automatically. No stale data. No manual cleanup. No reset call.&lt;/p&gt;

&lt;p&gt;There's a less obvious benefit worth naming: forms become first-class, inspectable runtime objects. While the form is alive, its full state — field values, validation errors, touched fields — is visible in &lt;a href="https://github.com/reduxjs/redux-devtools" rel="noopener noreferrer"&gt;Redux DevTools&lt;/a&gt;, time-travelable, and accessible to any other entity via &lt;code&gt;api.getEntity()&lt;/code&gt;. A confirmation dialog that needs to know whether the form is dirty before letting the user navigate away can just read the entity. No callbacks, no context, no prop drilling.&lt;/p&gt;

&lt;p&gt;For persistent forms — a settings page, a profile editor — you simply don't call &lt;code&gt;remove&lt;/code&gt;. The entity stays in the store, and its state reflects whatever the user last entered.&lt;/p&gt;

&lt;p&gt;Of course, this pushes more responsibility onto lifecycle design — you have to decide when something should die. That's a real cost. But it's an explicit, visible cost, not a hidden one.&lt;/p&gt;

&lt;p&gt;Multi-step forms are up to the developer. If each step has its own distinct lifecycle — you want to destroy step 1 before moving to step 2, or allow independent abandonment — you model them as separate entities. If the steps share a flat state (name on step 1, address on step 2), a single entity works just as well: the &lt;code&gt;render&lt;/code&gt; method shows the relevant fields based on a &lt;code&gt;currentStep&lt;/code&gt; property, and the entity is destroyed only when the whole flow completes. Either way, the store stays flat and normalized.&lt;/p&gt;




&lt;h2&gt;
  
  
  The FormSpy Problem Is Less Acute
&lt;/h2&gt;

&lt;p&gt;React Final Form's &lt;code&gt;FormSpy&lt;/code&gt; is the solution to a rendering problem: in React, form state changes trigger re-renders up the component tree, so you need a subscription mechanism to contain them.&lt;/p&gt;

&lt;p&gt;In Inglorious Web, full-tree re-rendering with lit-html's surgical DOM updates changes the cost significantly. When a field value changes, lit-html walks the template and touches only the DOM nodes that actually changed — the input's value attribute — skipping everything else. There's no virtual DOM reconciliation, no subscription system to configure, no &lt;code&gt;FormSpy&lt;/code&gt; equivalent to learn.&lt;/p&gt;

&lt;p&gt;This doesn't mean re-renders are free — it means the cost is low enough that it stops being a design constraint for the vast majority of forms. React can also optimize with memoization and signals, and those tools are worth using when you need them. The difference is that in Inglorious Web you don't need them by default. The &lt;a href="https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g"&gt;benchmarks post&lt;/a&gt; has the full picture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern Again
&lt;/h2&gt;

&lt;p&gt;Redux-form was right about the principle and wrong about the lifecycle. Formik and React Final Form solved the symptom by relocating form state rather than rethinking its lifetime. The tension was never resolved — only hidden.&lt;/p&gt;

&lt;p&gt;Inglorious Web makes lifetime explicit. Entities are created when needed and destroyed when done. The single source of truth principle turns out to be fine for forms.&lt;/p&gt;

&lt;p&gt;You just need entities that can die.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;There's not much more to say. One main concern I received so far is: isn't full tree re-rendering too expensive to be taken seriously? Wouldn't it be better if I memoized the render functions at least? Short answer: &lt;a href="https://dev.to/iceonfire/stop-blaming-re-renders-youre-optimizing-the-wrong-thing-2n8p"&gt;no.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>There's No Such Thing As Local State</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 31 Mar 2026 12:30:13 +0000</pubDate>
      <link>https://dev.to/iceonfire/theres-no-such-thing-as-local-state-2i4j</link>
      <guid>https://dev.to/iceonfire/theres-no-such-thing-as-local-state-2i4j</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of a series of corollaries to the &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;Inglorious Web series&lt;/a&gt;. It stands alone, but the examples make more sense if you've read the &lt;a href="https://dev.to/iceonfire/entity-centric-architecture-a-different-way-to-think-about-web-uis-4mkd"&gt;architecture post&lt;/a&gt; first. Or you can just go back to &lt;a href="https://dev.to/iceonfire/how-a-game-engine-accidentally-became-a-web-framework-1k8j"&gt;a time when we were just having fun creating videogames&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There's a decision every frontend developer makes early in a project, usually without realizing it's a decision: &lt;strong&gt;where does state live?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It feels like a technical question. It's actually an architectural one. Most frameworks default to local state and make global state expensive. Inglorious Web flips the default: &lt;strong&gt;state is global unless you intentionally isolate it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The difference looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgpo6ymx6dc78a8ax3v7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frgpo6ymx6dc78a8ax3v7.png" alt=" " width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every mainstream approach has a point where the model strains. The strain always comes from the same place — state that was supposed to stay contained, didn't. Here are three projects, three frameworks, and three different versions of why that matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fattutto: Context API Hits Its Limits
&lt;/h2&gt;

&lt;p&gt;In 2018 I was hired as lead front-end architect on &lt;a href="https://www.fattutto.com/" rel="noopener noreferrer"&gt;Fattutto&lt;/a&gt;, an invoicing application. We were a small team — two front-enders, two back-enders — working intensively for two months. &lt;a href="https://redux.js.org/" rel="noopener noreferrer"&gt;Redux&lt;/a&gt; existed but &lt;a href="https://redux-toolkit.js.org/" rel="noopener noreferrer"&gt;RTK&lt;/a&gt; didn't, and plain Redux felt too verbose for what we thought would be a modest amount of global state. &lt;a href="https://react.dev/learn/passing-data-deeply-with-context" rel="noopener noreferrer"&gt;Context API&lt;/a&gt; was a reasonable choice.&lt;/p&gt;

&lt;p&gt;And it was fine — for a while. The problem with Context API isn't that it's wrong for global state. The problem is performance: every consumer re-renders whenever that context's value changes, unless you split your contexts as finely as possible. As features accumulated, each new concern that needed to be shared got its own provider. By the time the app was mature, the root looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LicenceContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IdsToRemoveContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SelectionContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PageViewsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PreferencesContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SidebarsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Enhanced&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SidebarsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PreferencesContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PageViewsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SelectionContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;IdsToRemoveContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;LicenceContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;UserContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ThemeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight nested providers. The team that took over after me eventually reduced that number — proving Context is malleable enough to refactor, and that it wasn't a bad choice in the first place. But the growth happened, and reversing it required deliberate effort. That's the limit of the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tetra Pak: The Right Tool, Right from the Start
&lt;/h2&gt;

&lt;p&gt;In 2020 I was hired as lead front-end architect at &lt;a href="https://www.tetrapak.com/" rel="noopener noreferrer"&gt;Tetra Pak&lt;/a&gt; for an industrial &lt;a href="https://en.wikipedia.org/wiki/Human-Machine_Interface" rel="noopener noreferrer"&gt;HMI&lt;/a&gt; — a four-year project building the interface for factory control systems. I'd internalized the Fattutto lesson: even if the initial state seemed small, I went full RTK from day zero. That looked like overkill at first. By the end of the project, the store had grown to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drawer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changePwdModal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteModal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alarmsHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lineStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maneuverItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recipes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 30+ slices in total&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not overkill. The architecture was right, and I'm proud of it. The problem was ergonomic: creating a new RTK slice required enough ceremony that junior developers consistently reached for &lt;a href="https://react.dev/reference/react/useState" rel="noopener noreferrer"&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/a&gt; instead. In React with RTK, that's sometimes the correct call — creating a slice for a dropdown's open/closed state is genuine overhead, and &lt;code&gt;useState&lt;/code&gt; is the right tool for truly isolated UI state. The problem is that "truly isolated" is a bet, and the bet doesn't always pay off.&lt;/p&gt;

&lt;p&gt;The concrete example: a list of maneuver items, each rendered as an accordion. The user clicks a header to expand it, edits settings in the form inside, and clicks save. Saving should trigger multiple effects across different parts of the app: the accordion collapses, a recipe bar appears asking "do you want to save this change to the recipe too?", and potentially other reactions elsewhere.&lt;/p&gt;

&lt;p&gt;With local state, the first instinct looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ManeuverItem&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ManeuverForm&lt;/span&gt;
          &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;saveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// but how do we tell the recipe bar to appear?&lt;/span&gt;
            &lt;span class="c1"&gt;// and collapse all other items?&lt;/span&gt;
            &lt;span class="c1"&gt;// and trigger anything else that cares?&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment is the problem. &lt;code&gt;onSubmit&lt;/code&gt; doesn't know about the recipe bar. It can't reach the other accordion items. Every effect that needs to happen requires either prop drilling, a callback passed from above, or a new RTK slice. The state that felt local at definition time turned out to be needed in five other places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A state manager that's too painful to use correctly is almost as bad as no state manager at all.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Heineken: Multiple Sources of Truth
&lt;/h2&gt;

&lt;p&gt;More recently I was brought in on an &lt;a href="https://angular.dev/" rel="noopener noreferrer"&gt;Angular&lt;/a&gt; project for &lt;a href="https://www.heineken.com/" rel="noopener noreferrer"&gt;Heineken&lt;/a&gt;. This isn't an Angular problem specifically — almost every framework implicitly accepts multiple sources of truth as a normal condition. Forms manage their own state. Routing state lives in the URL. Filters live in a service. Default configuration lives somewhere else. The ecosystem treats this as pragmatic.&lt;/p&gt;

&lt;p&gt;Until it isn't. In one case, navigating to a URL was supposed to apply a default workcenter to the query string. Other parameters were added on top. But something kept removing the workcenter — a race between the URL history, the filter state, the default configuration, each one writing to the query string independently, each one unaware of the others. Days of debugging, because there was no single place to ask "what is the current routing state?"&lt;/p&gt;

&lt;p&gt;This isn't unusual. It's the norm. And it's worth naming, because it means the single source of truth principle isn't just a Redux dogma — it's the thing that makes routing state debuggable.&lt;/p&gt;

&lt;p&gt;Three different stacks, three different teams, the same failure mode: &lt;strong&gt;state escaping the place it was supposed to live.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Inglorious Web: Global by Default, Free of Charge
&lt;/h2&gt;

&lt;p&gt;All three stories share a root cause: state that someone decided was local, wasn't. The fix in each case required effort — refactoring contexts, adding slices, untangling sources of truth — because the architecture didn't make global state cheap enough to reach for from the start.&lt;/p&gt;

&lt;p&gt;Before addressing the obvious objection ("you don't need a global store, you need signals"), let me show what the &lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt; version looks like. Then I'll come back to it.&lt;/p&gt;

&lt;p&gt;Here's the maneuver accordion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ManeuverItem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;maneuverItem:collapse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// enqueued first — collapses all&lt;/span&gt;
      &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:open`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// enqueued second — opens this one&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;collapse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;saveItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;maneuverItemSave&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// broadcast — anyone who cares reacts&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;div class="maneuver-item"&amp;gt;
        &amp;lt;button @click=&lt;/span&gt;&lt;span class="p"&gt;${()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:toggle`&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;
          &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
        &amp;lt;/button&amp;gt;
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
          &amp;lt;maneuver-form
            .item=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            @submit=&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:save`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
          &amp;gt;&amp;lt;/maneuver-form&amp;gt;
        `&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;/div&amp;gt;
    `&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event queue is what makes &lt;code&gt;toggle&lt;/code&gt; work correctly. &lt;code&gt;api.notify()&lt;/code&gt; enqueues events rather than firing them immediately — so &lt;code&gt;maneuverItem:collapse&lt;/code&gt; hits all entities first, then &lt;code&gt;#maneuver1:open&lt;/code&gt; fires, leaving exactly one item open. The order is deterministic and explicit.&lt;/p&gt;

&lt;p&gt;And elsewhere in the app, the recipe bar simply listens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RecipeBar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;maneuverItemSave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pendingChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isVisible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No callbacks. No prop drilling. No knowledge of the component tree. &lt;code&gt;maneuverItemSave&lt;/code&gt; is a broadcast — every entity with that handler reacts independently. Adding a new reaction means adding a handler to a new type, nothing else.&lt;/p&gt;

&lt;p&gt;This looks like classes with methods. It isn't. The type object holds no state — &lt;code&gt;create()&lt;/code&gt; fires when the entity is added to the store and initializes its state there. &lt;code&gt;toggle()&lt;/code&gt;, &lt;code&gt;open()&lt;/code&gt;, &lt;code&gt;collapse()&lt;/code&gt;, &lt;code&gt;save()&lt;/code&gt; are reducers. Everything lives in the store, accessible to any other entity, debuggable, and testable by calling a function.&lt;/p&gt;

&lt;p&gt;Multiple instances are declared explicitly or created at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Static&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;maneuver1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ManeuverItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Approach&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;maneuver2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ManeuverItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Lift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;maneuver3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ManeuverItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deposit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Or dynamically as data arrives&lt;/span&gt;
&lt;span class="nx"&gt;maneuverItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ManeuverItem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each instance gets its own independent state in the store. Adding a new maneuver item type means writing another type object — same shape, same cost, no additional infrastructure.&lt;/p&gt;

&lt;p&gt;The router follows the same principle — web history and store state kept in sync as a single entity, with every navigation visible as an event in &lt;a href="https://github.com/reduxjs/redux-devtools" rel="noopener noreferrer"&gt;Redux DevTools&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#router:navigate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/workcenter/42?filter=active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Heineken race condition — multiple services writing to the query string independently — would have been visible immediately. One entity, one source of truth, every change traceable.&lt;/p&gt;

&lt;p&gt;When state changes, Inglorious Web re-renders the whole tree and lets lit-html handle the DOM diff surgically. No &lt;code&gt;useEffect&lt;/code&gt; firing unexpectedly, no stale closures. The form saves, the event broadcasts, the store updates, the tree re-renders, the accordion closes, the recipe bar appears. Every step is explicit and traceable. The &lt;a href="https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g"&gt;benchmarks post&lt;/a&gt; has the full numbers — the short version is 120 FPS on dashboards without any optimization work.&lt;/p&gt;




&lt;h2&gt;
  
  
  "But You Don't Need a Store — You Need Signals"
&lt;/h2&gt;

&lt;p&gt;The objection is fair, and it's worth addressing directly: signals solve re-render granularity without a store. A signal for &lt;code&gt;isOpen&lt;/code&gt; on each accordion item would work. Fine-grained reactivity would update only the affected DOM node.&lt;/p&gt;

&lt;p&gt;The problem isn't the re-renders — as the benchmarks show, full-tree rendering with &lt;a href="https://www.npmjs.com/package/lit-html" rel="noopener noreferrer"&gt;lit-html&lt;/a&gt; is fast enough that the difference is imperceptible in practice. The problem is the dependency graph. Signals are implicit: the runtime tracks which signals were read during rendering, and re-runs when they change. That graph is invisible, lives in the framework's memory, and doesn't show up in your DevTools. When something updates unexpectedly — or fails to update — you're debugging a system you can't inspect.&lt;/p&gt;

&lt;p&gt;The event model is explicit: every state transition is a named event you can grep, every handler is a function you can call in a test, every change is visible in Redux DevTools. That's the tradeoff. For the full argument, it's in the &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;first post of the series&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision You're Actually Making
&lt;/h2&gt;

&lt;p&gt;When you reach for &lt;code&gt;useState&lt;/code&gt;, you're making a bet: that this state will never need to be shared. In React, that bet sometimes pays off — a dropdown that genuinely never coordinates with anything else is a fine use of local state, and creating a Redux slice for it is real overhead. The friction isn't imaginary.&lt;/p&gt;

&lt;p&gt;But the asymmetry is brutal in one direction: keeping state local when it should be global is cheap to write and expensive to undo. The Fattutto pyramid, the Tetra Pak accordion, the Heineken routing race — all of them started as decisions that felt small.&lt;/p&gt;

&lt;p&gt;In Inglorious Web the asymmetry disappears. Global state looks like local state — a &lt;code&gt;create()&lt;/code&gt; handler reads almost like a &lt;code&gt;useState&lt;/code&gt; initializer, a type object reads almost like a component. The difference is that everything ends up in the store, accessible to everything else, at no extra cost. You're not choosing between local convenience and global power. You just write the type, and the store takes care of the rest.&lt;/p&gt;

&lt;p&gt;The decision still exists. But now the safe choice is also the easy one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;If the state is better global, then what about edge cases like forms? Turns out, you don't need local state for those. &lt;a href="https://dev.to/iceonfire/you-just-need-entities-that-can-die-4l80"&gt;You just need entities that can die.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Suspense Is A Symptom</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 24 Mar 2026 13:32:00 +0000</pubDate>
      <link>https://dev.to/iceonfire/suspense-is-a-symptom-clj</link>
      <guid>https://dev.to/iceonfire/suspense-is-a-symptom-clj</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of a series on &lt;a href="https://inglorious.dev/web" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt;. You can &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;start from the beginning&lt;/a&gt;, &lt;a href="https://dev.to/iceonfire/you-might-not-need-tanstack-query-2f3l"&gt;go back one post&lt;/a&gt; to see another problem that simply vanishes when you reframe the context, or read it standalone — the argument here is self-contained.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Inglorious Web didn't start as a React alternative. It started as a game engine.&lt;/p&gt;

&lt;p&gt;I wanted to build a cool JavaScript game engine — something that could handle hundreds of entities updating at 60 frames per second, with clean state management, deterministic event handling, and no magic. The state manager that emerged from that work was general enough that I started using it for web apps too. And once it was powering web UIs, the React rendering layer started feeling like the wrong tool: too heavy, too coupled, too opinionated about who owns state. I replaced it with &lt;a href="https://www.npmjs.com/package/lit-html" rel="noopener noreferrer"&gt;lit-html&lt;/a&gt; and followed the logic all the way through.&lt;/p&gt;

&lt;p&gt;Game engines have been solving high-frequency state updates with heterogeneous objects for decades — under far stricter performance constraints than any web UI. They never needed a component hierarchy. They never needed to fire REST requests from inside a render function. And they never needed &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That last point is the one I want to pull on.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Suspense Actually Does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; was introduced by &lt;a href="https://react.dev/reference/react/Suspense" rel="noopener noreferrer"&gt;React&lt;/a&gt; and has since been adopted by &lt;a href="https://docs.solidjs.com/reference/components/suspense" rel="noopener noreferrer"&gt;SolidJS&lt;/a&gt;, and more recently &lt;a href="https://vuejs.org/guide/built-ins/suspense.html" rel="noopener noreferrer"&gt;Vue&lt;/a&gt;. The premise is elegant: wrap a part of your component tree in a boundary, declare a fallback, and the framework shows the fallback until all async resources inside the boundary have resolved. No cascading spinners, no layout shifts, no manual coordination of loading states across components.&lt;/p&gt;

&lt;p&gt;Here's the SolidJS version, straight from their docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MyComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fetch */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail: &lt;code&gt;createResource&lt;/code&gt; reads data &lt;em&gt;inside the component&lt;/em&gt;, inside the render function. The component itself initiates the fetch. Suspense catches the pending state at the nearest boundary and holds back the DOM until the resource resolves — pre-rendering nodes speculatively so there's less work to do when data arrives.&lt;/p&gt;

&lt;p&gt;It's genuinely clever engineering. But notice what it's engineering around.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Suspense Solves — And Where It Comes From
&lt;/h2&gt;

&lt;p&gt;The coordination problem Suspense solves exists because of a specific architectural choice: &lt;strong&gt;data fetching is coupled to rendering&lt;/strong&gt;. The component declares what it needs, initiates the fetch, and renders based on the result — all in one place. When multiple components do this independently, you get cascading loading states that need a boundary mechanism to coordinate.&lt;/p&gt;

&lt;p&gt;Game engines don't have this problem. Not because they don't fetch data asynchronously — they do it constantly. Loading screens stream level geometry. Multiplayer games make network calls every frame. Asset pipelines fetch textures and audio in the background. But the loading state is just state. A loading screen is an entity. The transition from loading to loaded is an event. The render function reads whatever state the entity currently holds.&lt;/p&gt;

&lt;p&gt;Game engines solve this differently. Loading screens, asset streaming systems, scene readiness checks — these all exist, and they handle async coordination constantly. But they do it by reading state that reflects loading progress, not by letting the rendering layer initiate the loading itself. The render function reads whatever the entity currently holds. The loading logic lives elsewhere.&lt;/p&gt;

&lt;p&gt;This is the same architectural instinct that Inglorious Web carries into web UI development.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Inglorious Web Handles It
&lt;/h2&gt;

&lt;p&gt;In Inglorious Web, fetching data and rendering data are two separate concerns handled by two separate mechanisms. &lt;code&gt;handleAsync&lt;/code&gt; manages the fetch lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handleAsync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inglorious/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;handleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetchProfile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/profiles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The render function reads state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;`&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;div&amp;gt;Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;div&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary. There is no resource being read inside the render function. The fetch is triggered by an event — &lt;code&gt;store.notify('profile:fetchProfile', userId)&lt;/code&gt; — and the result lands in the store. The render function is a pure function of whatever state the entity currently holds. It doesn't know or care how that state got there.&lt;/p&gt;

&lt;p&gt;This isn't a workaround for the absence of Suspense. It's a different model where the coordination problem doesn't arise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Suspense Is a Symptom
&lt;/h2&gt;

&lt;p&gt;The spread of Suspense across frameworks — React, then SolidJS, now Vue — tells you something. Frameworks that allow async resources to be read inside the reactive graph tend to require Suspense-like coordination to manage the consequences. Not all signal-based frameworks go this route — Svelte notably doesn't — but the pattern is clear enough: when data fetching becomes part of the reactive rendering graph, a boundary mechanism follows.&lt;/p&gt;

&lt;p&gt;This is the pattern that recurs across the whole series. &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;TanStack Query&lt;/a&gt; exists because component-local state creates a data fragmentation problem. Suspense exists because reactive rendering creates a loading coordination problem. Each tool is a genuine solution to a real problem — but the problem is architectural, not universal. &lt;a href="https://dev.to/iceonfire/you-might-not-need-tanstack-query-2f3l"&gt;Change the architecture, and the problem disappears&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Game engines figured this out not by being clever, but by being constrained. When you're updating thousands of entities at 60 frames per second, you can't afford to let the rendering layer own business logic. State is state. Rendering is rendering. The separation isn't a best practice — it's a survival requirement.&lt;/p&gt;

&lt;p&gt;Web UIs operate under far looser constraints, which is probably why the coupling was allowed to creep in. Components that fetch their own data are convenient. Reactive resources that update automatically are ergonomic. Suspense boundaries that coordinate loading states are elegant. Each step feels like an improvement. But the complexity accumulates, and each layer of machinery exists to manage the consequences of the previous one.&lt;/p&gt;

&lt;p&gt;Inglorious Web went the other way — not because I planned it, but because the game engine origin forced the separation from the start. The state manager didn't know anything about rendering. The renderer didn't know anything about data fetching. When I applied that architecture to web UIs, there was nothing left for Suspense to do.&lt;/p&gt;




&lt;h2&gt;
  
  
  When You Might Still Want Suspense
&lt;/h2&gt;

&lt;p&gt;Being honest here matters. If you're using a framework where data fetching is coupled to rendering — which describes most of the mainstream options — Suspense is the right tool. It's a well-designed solution to a real problem in that context.&lt;/p&gt;

&lt;p&gt;And there's one scenario where even an entity-based architecture needs something like Suspense: &lt;strong&gt;lazy-loaded entity types&lt;/strong&gt;. If your app loads type definitions on demand — for route-based code splitting, plugin systems, or large config-driven UIs — you need a way to signal "this type isn't ready yet, show a fallback."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://inglorious.dev/web/featured/router.html" rel="noopener noreferrer"&gt;Inglorious Web's router&lt;/a&gt; already handles this. Here's what the app-level render function looks like when supporting lazy-loaded routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;router&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;main&amp;gt;
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;div&amp;gt;Route not found: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;
        &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;/main&amp;gt;
    `&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the lazy type itself is just a plain type definition loaded on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lazy-type.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LazyType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;h1&amp;gt;Lazy Loaded Route&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Check your network panel: this route was loaded on demand!&amp;lt;/p&amp;gt;
    `&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's happening: &lt;code&gt;router.isLoading&lt;/code&gt; is just state on an entity. The fallback is just a conditional in the render function. The lazy type loads when the route is navigated to, and the transition from loading to loaded is an event. There's no boundary mechanism catching a thrown promise — the app-level render function &lt;em&gt;is&lt;/em&gt; the boundary, and it's explicit rather than implicit.&lt;/p&gt;

&lt;p&gt;This is a legitimate use case for Suspense-like coordination, and Inglorious Web handles it natively through the same mechanisms it uses for everything else. The difference is that you can see exactly what triggers the loading state, exactly what the fallback is, and exactly when the transition happens. Nothing is implicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;Every post in this series has landed on the same observation from a different angle: the tools we reach for most often exist to manage consequences of architectural choices, not to solve fundamental problems. Remove the choice, and the tool becomes unnecessary.&lt;/p&gt;

&lt;p&gt;REST didn't build a better session cache — it eliminated the need for one. The entity store doesn't build a better query cache — it makes the fragmentation that requires one impossible. And Inglorious Web doesn't need a better Suspense — because when data fetching lives in the store and rendering is a pure function of state, there's nothing to suspend.&lt;/p&gt;

&lt;p&gt;The game engine didn't solve this problem. It never had it — because the constraint of rendering thousands of entities at 60 frames per second made the coupling between fetching and rendering unthinkable from the start. The separation wasn't a design principle. It was a survival requirement.&lt;/p&gt;

&lt;p&gt;Web UIs operate under far looser constraints, which is probably why the coupling was allowed to develop. But looser constraints don't mean the coupling is free. It just means the cost shows up later, in the form of tools like Suspense that exist to manage consequences rather than prevent them.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;If you're curious about how Inglorious Web was born, you can check &lt;a href="https://dev.to/iceonfire/how-a-game-engine-accidentally-became-a-web-framework-1k8j"&gt;its origin story here&lt;/a&gt;. Otherwise I can tell you &lt;a href="https://dev.to/iceonfire/theres-no-such-thing-as-local-state-2i4j"&gt;why giving state and behavior to components is usually a bad idea&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>react</category>
    </item>
    <item>
      <title>How a Game Engine Accidentally Became a Web Framework</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 17 Mar 2026 13:30:03 +0000</pubDate>
      <link>https://dev.to/iceonfire/how-a-game-engine-accidentally-became-a-web-framework-1k8j</link>
      <guid>https://dev.to/iceonfire/how-a-game-engine-accidentally-became-a-web-framework-1k8j</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is the origin story behind &lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt;. You can read it standalone, or as a prologue to the &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;series that followed&lt;/a&gt;. Or just go back to the &lt;a href="https://dev.to/iceonfire/suspense-is-a-symptom-clj"&gt;previous post in the series&lt;/a&gt;, who am I to judge.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;This is not a post about a framework. It's a post about why I needed to build one — and why it took three years, a conference that almost nobody was supposed to attend, and the worst year of my life to get there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summer 2022: The Idea
&lt;/h2&gt;

&lt;p&gt;I love &lt;a href="https://redux.js.org/" rel="noopener noreferrer"&gt;Redux&lt;/a&gt;. Not in the abstract — I mean the specific intellectual pleasure of the &lt;a href="https://redux.js.org/understanding/thinking-in-redux/three-principles" rel="noopener noreferrer"&gt;three principles&lt;/a&gt;: single source of truth, immutable state, pure functions. The kind of architecture where you can look at any state transition and reason about it completely. Where testing is just calling a function.&lt;/p&gt;

&lt;p&gt;I'd been using it professionally for years, and at some point I started wondering: what if you applied those same principles to a game engine? Games have to manage hundreds of entities updating every frame. They have to be deterministic, debuggable, and fast. Redux had proved those properties in web apps — could they survive the pressure of a game loop?&lt;/p&gt;

&lt;p&gt;On July 30, 2022, I started finding out. The core idea was simple: model the game loop as a pure function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gameLoop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was my first attempt. It didn't last long. Redux itself turned out to be the wrong fit — slices and reducers weren't designed for entities that need independent state and behavior. So I threw it out and designed my own store: an entity-based model where each entity has a type, and each type defines its behavior as event handlers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;player1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Player&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That felt right. By August 2022, the entity store worked. At the time I thought I was building a game engine. In hindsight, I was building a state architecture that accidentally had to do with games. And then — like most side projects — life got in the way, and I set it down.&lt;/p&gt;




&lt;h2&gt;
  
  
  2023: Game AI and the Canvas
&lt;/h2&gt;

&lt;p&gt;A year later, a book on game AI algorithms reignited the project. I wanted to implement pathfinding, behavior trees, and decision systems in a functional style rather than the object-oriented approach most game development books assume. The entity architecture made it elegant — an enemy entity checking distance to a player entity, notifying an event, the store processing it in order. Deterministic, testable, no magic.&lt;/p&gt;

&lt;p&gt;I added a 2D canvas rendering layer. React had been powering the visuals, but there's only so much the DOM can do at 60 frames per second. More importantly, I wanted the engine to be render-agnostic — the store shouldn't care whether it's rendering to a canvas, a DOM, or nothing at all.&lt;/p&gt;

&lt;p&gt;By late 2023, the engine had a working canvas renderer, an AI system, and a clean separation between state, behavior, and rendering. Then I hit collision detection, and the motivation drained again. Collision detection is one of those problems that starts simple (two rectangles, check the overlap) and reveals an abyss the moment you look closely. I'd also just gotten married. The engine went back in the drawer.&lt;/p&gt;




&lt;h2&gt;
  
  
  2025: Coding Is Coping
&lt;/h2&gt;

&lt;p&gt;Right after my wedding, things went south quickly. My mother was diagnosed with cancer. She died in March 2025.&lt;/p&gt;

&lt;p&gt;In the same period, LLMs arrived and started threatening the thing I'd built my professional identity around. The combination — personal grief, professional anxiety, the general sense that the ground was shifting — produced a burnout I didn't fully recognize until I was already deep in it.&lt;/p&gt;

&lt;p&gt;What pulled me out, eventually, was coding. Not productively at first. Just tinkering, reading old code, seeing if things still made sense. LLMs helped in a way I didn't expect: not by writing the code for me, but by giving me a thinking partner when I needed to reason through a design decision at 11pm with nobody else around. The engine, sitting untouched for months, started to feel interesting again.&lt;/p&gt;

&lt;p&gt;I know this sounds like I'm romanticizing it. But there's something specific about working on a system that behaves exactly the way you tell it to — that has no opinion about your grief, no awareness of your anxiety, that just processes events in order and gives you back a new state. It was the right kind of boring.&lt;/p&gt;




&lt;h2&gt;
  
  
  July 2025: The Conference
&lt;/h2&gt;

&lt;p&gt;My friend &lt;a href="https://www.linkedin.com/in/savi-carlone/" rel="noopener noreferrer"&gt;Savino Carlone&lt;/a&gt; organizes a small conference in Turin for .NET developers — &lt;em&gt;&lt;a href="https://www.meetup.com/it-it/torino-net-user-group/" rel="noopener noreferrer"&gt;Torino .NET&lt;/a&gt;&lt;/em&gt;. He needed a speaker for a mid-August online slot and asked if I could fill it. I said I had nothing to talk about. He said: "Don't worry, nobody's going to listen anyway. Everyone's on vacation."&lt;/p&gt;

&lt;p&gt;That was enough. I said yes, pulled up the engine code, and started re-reading it to prepare slides.&lt;/p&gt;

&lt;p&gt;What happened next surprised me. Re-reading code you wrote in a different state of mind is a strange experience. Some of it was embarrassing. Some of it was better than I remembered. And somewhere in the process of preparing examples and building a demo — a particle system, a few simple game prototypes — I realized that I'd built something genuinely interesting without fully noticing.&lt;/p&gt;

&lt;p&gt;The attendance was better than expected. I opened with a hook for the .NET crowd: certain problems can be solved through the decorator pattern in object-oriented programming — and then I showed how, in functional programming, the decorator pattern becomes function composition. That part landed. When I moved deeper into the FP principles behind the engine, I lost most of them technically, but something still came through. They appreciated it. I left with my passion back, which was the only thing that mattered.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Coding Spree
&lt;/h2&gt;

&lt;p&gt;After the conference, I couldn't stop. And here I want to say something about LLMs that I don't see said often enough.&lt;/p&gt;

&lt;p&gt;Nobody else cared about this project. Not in the way that matters when you're deep in a design decision at 11pm and need to think out loud. LLMs gave me something I hadn't had in a long time: a partner with encyclopedic knowledge who was genuinely willing to engage — not just say "well done," but push back, suggest alternatives, catch mistakes, and sustain a real technical conversation for as long as I needed. They also gave me the ability to move fast on the parts of the codebase I found tedious, so I could spend my energy on the parts I found interesting.&lt;/p&gt;

&lt;p&gt;Yes, LLMs had threatened my sense of professional relevance. They also saved the project. Both things are true.&lt;/p&gt;

&lt;p&gt;I added a scaffolding tool, sound support, touch support, an entity pool for performance-critical scenarios. I even created a JavaScript superset — &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@inglorious/babel-plugin-inglorious-script" rel="noopener noreferrer"&gt;IngloriousScript&lt;/a&gt;&lt;/strong&gt; — that adds native vector operations to the language, because I kept writing &lt;code&gt;add(position, scale(velocity, dt))&lt;/code&gt; when I wanted to write &lt;code&gt;position + velocity * dt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And then, sometime in October 2025, I noticed something.&lt;/p&gt;

&lt;p&gt;The state manager at the heart of the engine — the entity store I'd built in 2022 — was completely decoupled from the game loop and the rendering layer. It just managed state. It processed events. It was predictable and testable and deterministic.&lt;/p&gt;

&lt;p&gt;It could power a web app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not Only Games
&lt;/h2&gt;

&lt;p&gt;I'd spent years building React architectures for complex, data-heavy applications — config-driven dashboards at &lt;a href="https://www.irion-edm.com/" rel="noopener noreferrer"&gt;Irion&lt;/a&gt;, industrial HMI interfaces as lead front-end architect at &lt;a href="https://www.tetrapak.com/" rel="noopener noreferrer"&gt;Tetra Pak&lt;/a&gt;. I was proud of that work. But I could also see exactly where it would age. The complexity of RTK made junior developers reach for &lt;code&gt;useState()&lt;/code&gt; when they shouldn't. Cascading renders made behavior unpredictable at times. Business logic drifted into components because that's where the framework pulled it. The kind of system that works until it doesn't, and then debugging it requires holding the entire tree in your head.&lt;/p&gt;

&lt;p&gt;The entity store solved those problems structurally — not by being clever, but by keeping concerns separate from the start: state in the store, behavior in types, rendering as a pure function of state. A couple of years after my collaboration with Tetra Pak ended, I went back to propose migrating to Inglorious Store and Inglorious Web. The reception was warm. The migration never started. So the experiments my team and I are doing remain in-house for now — which is fine. The framework exists either way.&lt;/p&gt;

&lt;p&gt;Then I kept pulling the thread. If all the logic lives in the store, what is React actually doing? Providing lifecycle hooks? Not needed — there are no component-level lifecycles in an entity model. Diffing the virtual DOM? Overkill when the render function is already pure and &lt;a href="https://www.npmjs.com/package/lit-html" rel="noopener noreferrer"&gt;lit-html&lt;/a&gt; can handle surgical DOM updates at 3.24kB total.&lt;/p&gt;

&lt;p&gt;By November 2025, React was gone. &lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Inglorious Web&lt;/a&gt;&lt;/strong&gt; was born.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Became
&lt;/h2&gt;

&lt;p&gt;The framework now ships with a router, form handling, table, select, virtualized list, declarative SVG charts, an animation library, a design system, and SSG with HMR and file-based routing. It's Redux DevTools compatible, works with any web component, and renders to the real DOM — no virtual DOM, no compiler, no proxy-based reactivity.&lt;/p&gt;

&lt;p&gt;The bundle is 16KB. The mental model is a one-time cost. Testing is just calling functions.&lt;/p&gt;

&lt;p&gt;I wrote a series of posts about the architecture and the benchmarks — why I made the choices I made, where the framework fits, and where it honestly doesn't. If you want the technical argument, &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;start there&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is just about how it got here: a Redux idea applied to games, a drawer it sat in for too long, a grief that made coding feel necessary, a conference in Turin that reignited everything, and a thread I kept pulling until something new came out the other end.&lt;/p&gt;

&lt;p&gt;In May 2026 I'll be speaking about all of this at &lt;a href="https://jstek.io/" rel="noopener noreferrer"&gt;JsTek&lt;/a&gt; in Chicago — hoping it finds a slightly larger audience than a mid-August .NET meetup. If you're going to be there, come say hello.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EDIT (June 2026):&lt;/strong&gt; The conference is over, and boy, what an experience. I was flown out to the US with travel and accommodation paid to give two speeches... only for fewer people to show up than at that mid-August online meetup! To top it off, someone literally stole my audience by staging an improvised talk in the room next door and inviting everyone to join. I don’t think I’ve ever felt quite so betrayed and humiliated in my professional life.&lt;/p&gt;

&lt;p&gt;But the universe has a strange sense of irony. Right after returning to Italy, I attended a local .NET meetup where the scheduled speaker's train was delayed by 200 minutes. I offered to step in and give an impromptu talk to fill the gap. It was a massive success. They were incredibly grateful for saving the evening, the energy was electric, and I finally got the engaged audience I was looking for. Turns out, this framework just thrives when things get weird.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;Videogames taught me one thing, that is common to dashboards and pretty much any complex piece of software: &lt;a href="https://dev.to/iceonfire/theres-no-such-thing-as-local-state-2i4j"&gt;there's no such thing as local state&lt;/a&gt;. Come find out why.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>career</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>You Might Not Need TanStack Query</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 10 Mar 2026 13:30:03 +0000</pubDate>
      <link>https://dev.to/iceonfire/you-might-not-need-tanstack-query-2f3l</link>
      <guid>https://dev.to/iceonfire/you-might-not-need-tanstack-query-2f3l</guid>
      <description>&lt;p&gt;When I teach web development, I always spend time on one of the most elegant moves in the history of software architecture: the shift from Web 1.0 application servers to REST.&lt;/p&gt;

&lt;p&gt;Early application servers needed to share session state across machines — a genuinely hard distributed systems problem. People built clever solutions: sticky sessions, shared databases, distributed caches. Then REST came along and didn't solve the problem at all. It &lt;em&gt;eliminated&lt;/em&gt; it. By making each request self-contained, stateless by design, the need for shared session state simply vanished. The solution was a reframing.&lt;/p&gt;

&lt;p&gt;I think about that move a lot when I look at how modern frontend frameworks handle server state — because I think the same thing is happening again, and most people are still on the "build a better session cache" side of it.&lt;/p&gt;

&lt;p&gt;TanStack Query is excellent. If you're using React with component-local state or context, you should probably use it. But understanding &lt;em&gt;why&lt;/em&gt; it exists reveals something interesting: the problems it solves are architectural, not universal.&lt;/p&gt;

&lt;p&gt;If your app is built around a centralized, entity-based store, you may already have everything TanStack Query gives you. And the only missing piece might be a single helper function.&lt;/p&gt;




&lt;h2&gt;
  
  
  What TanStack Query Actually Solves
&lt;/h2&gt;

&lt;p&gt;Before dismissing any tool, it's worth being precise about the problem it's solving.&lt;/p&gt;

&lt;p&gt;In a typical React app, server state lives in components. Two components that need the same data will independently &lt;code&gt;useEffect&lt;/code&gt; their way to it, each triggering its own fetch. There's no shared cache, no way to say "this data is already in flight — don't fetch it again." The result is duplicate requests, inconsistent UI states, and a lot of hand-rolled loading/error handling boilerplate.&lt;/p&gt;

&lt;p&gt;TanStack Query solves this with a query cache: a centralized store keyed by query keys, where identical keys share a single fetch lifecycle. It adds background refetching, stale-time configuration, request deduplication, and a clean &lt;code&gt;{ data, isLoading, error }&lt;/code&gt; API on top.&lt;/p&gt;

&lt;p&gt;It's solving, essentially, the absence of a single source of truth for server state.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Entity Store Already Has One
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://inglorious.dev/store/" rel="noopener noreferrer"&gt;Inglorious Store&lt;/a&gt;, state lives in a store as entities. An entity is just a plain object with an &lt;code&gt;id&lt;/code&gt; and a &lt;code&gt;type&lt;/code&gt;. Its behavior — how it responds to events — is defined by its type.&lt;/p&gt;

&lt;p&gt;When you need to fetch some data, you model it as an entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 'loading' | 'success' | 'error'&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This entity lives in the store. It's not local to a component. Every component that needs posts reads the same &lt;code&gt;posts&lt;/code&gt; entity.&lt;/p&gt;

&lt;p&gt;That's deduplication. Not as a feature you opted into — as a consequence of the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;handleAsync&lt;/code&gt; Helper Is All You Need
&lt;/h2&gt;

&lt;p&gt;The remaining question is how to manage the fetch lifecycle: fire the request, set loading state, handle success, handle error. Without a helper, that's repetitive. With one, it collapses to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handleAsync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inglorious/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;handleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetchPosts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;handleAsync&lt;/code&gt; expands into a set of event handlers — &lt;code&gt;fetchPosts&lt;/code&gt;, &lt;code&gt;fetchPostsStart&lt;/code&gt;, &lt;code&gt;fetchPostsRun&lt;/code&gt;, &lt;code&gt;fetchPostsSuccess&lt;/code&gt;, &lt;code&gt;fetchPostsError&lt;/code&gt; — that you can trigger, test, and extend like any other store behavior.&lt;/p&gt;

&lt;p&gt;To trigger a fetch, you notify the store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Posts:fetchPosts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To read the data, you read the entity. In a React component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Error: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Comparison Side by Side
&lt;/h2&gt;

&lt;p&gt;Here's the same scenario in TanStack Query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;}&amp;lt;/u&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clean, and TanStack deserves credit for how ergonomic it is. But notice what it's doing: it's creating an implicit cache (keyed by &lt;code&gt;['posts']&lt;/code&gt;) to compensate for the fact that there's no shared store. If two &lt;code&gt;PostList&lt;/code&gt; components mount, TanStack Query deduplicates the fetch via that cache.&lt;/p&gt;

&lt;p&gt;In the entity-based version, there's no need for a cache layer at all. The store &lt;em&gt;is&lt;/em&gt; the cache. &lt;code&gt;posts&lt;/code&gt; is a single entity. Two components reading from it are just reading the same thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  This Isn't a New Idea
&lt;/h2&gt;

&lt;p&gt;It's worth stepping back, because the entity store isn't the only architecture that sidesteps this problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angular&lt;/strong&gt; has always had services — injectable classes that live outside components and centralize logic. An Angular developer who designs their services well, using RxJS's &lt;code&gt;shareReplay(1)&lt;/code&gt; to multicast a single HTTP request to multiple subscribers, largely doesn't feel the pain TanStack Query solves. The Angular ecosystem has a TanStack Query adapter, but experienced Angular developers often don't reach for it because their tooling already pushes them toward centralization. The architectural instinct was there from the beginning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redux&lt;/strong&gt; centralizes state, just like the entity store. So why does RTK Query exist?&lt;/p&gt;

&lt;p&gt;This is worth being precise about, because it's a genuine objection to the argument. Redux solves the shared state problem — there's one store, one source of truth — but it says nothing about how data gets &lt;em&gt;into&lt;/em&gt; that store. You still write thunks or sagas to fire requests, and you still manually dispatch loading, success, and error actions. That boilerplate is real, and RTK Query eliminates it by auto-generating everything from an endpoint definition.&lt;/p&gt;

&lt;p&gt;So RTK Query isn't solving the same problem as TanStack Query. TanStack Query compensates for the absence of a shared store. RTK Query eliminates lifecycle boilerplate on top of a store that already exists. These are different problems.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;handleAsync&lt;/code&gt; is Inglorious Store's answer to that same boilerplate problem. The difference is that RTK Query goes further — it adds cache lifetime management, automatically removing data from the store when no component is subscribed to it anymore. &lt;code&gt;handleAsync&lt;/code&gt; doesn't do that. For most applications it doesn't matter, but it's an honest gap.&lt;/p&gt;

&lt;p&gt;The broader point is that this pattern — centralize state, reduce fetching to a lifecycle concern — has precedent across frameworks and decades. The entity store is a particularly clean expression of it, but the instinct is not new.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You'd Still Want
&lt;/h2&gt;

&lt;p&gt;Being precise here matters. There are features TanStack Query provides that &lt;code&gt;handleAsync&lt;/code&gt; doesn't out of the box:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refetch on window focus.&lt;/strong&gt; When the user tabs back to the app, TanStack Query can silently refetch stale data. You could replicate this with a &lt;code&gt;visibilitychange&lt;/code&gt; listener that notifies the store, but it's not automatic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale time and cache invalidation.&lt;/strong&gt; TanStack lets you say "consider this data stale after 30 seconds." In an entity store, you'd add a &lt;code&gt;fetchedAt&lt;/code&gt; field to the entity and check it before deciding whether to fetch. A small utility could wrap that pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pagination and infinite queries.&lt;/strong&gt; TanStack has first-class support for these. In an entity store, you'd model paginated state explicitly — &lt;code&gt;page&lt;/code&gt;, &lt;code&gt;hasMore&lt;/code&gt;, &lt;code&gt;items&lt;/code&gt; — which is more verbose but also more transparent.&lt;/p&gt;

&lt;p&gt;These are real gaps. But they're gaps you'd fill with a few conventions and perhaps a small utility, not a 40KB library with its own cache, observer system, and devtools.&lt;/p&gt;




&lt;h2&gt;
  
  
  When You &lt;em&gt;Should&lt;/em&gt; Use TanStack Query
&lt;/h2&gt;

&lt;p&gt;If you're building a React app without a centralized store, TanStack Query is the right answer. It provides the shared cache you're missing, with a polished API and a huge ecosystem.&lt;/p&gt;

&lt;p&gt;If you're already on Redux Toolkit, RTK Query is worth looking at — it integrates the same ideas directly into the Redux store.&lt;/p&gt;

&lt;p&gt;And honestly, even in an entity-based architecture, if your data-fetching needs become complex enough — lots of pagination, optimistic updates, real-time invalidation — reaching for TanStack Query as an explicit caching layer isn't wrong. It just solves a problem you don't have &lt;em&gt;yet&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Remember REST. It didn't build a better session cache — it reframed the problem until the cache wasn't needed.&lt;/p&gt;

&lt;p&gt;The entity store does something similar on the client side. TanStack Query exists because component-local state creates a fragmentation problem: multiple components, multiple fetches, no shared truth. Its query cache is a sophisticated solution to that problem. But the entity store reframes it: state is centralized by design, so the fragmentation never happens. There's no cache to build because there's no incoherence to resolve.&lt;/p&gt;

&lt;p&gt;The irony is that REST solved distributed state by embracing &lt;em&gt;statelessness&lt;/em&gt; on the server. The entity store solves the client-side version of the same problem by doing the opposite — embracing a single, explicit, stateful store. Same intellectual move (reframe the problem), opposite mechanism.&lt;/p&gt;

&lt;p&gt;If your state lives in components, you need TanStack Query. If your state lives in a centralized entity store, you already have deduplication and a single source of truth for free. All you need on top is a clean way to manage the async lifecycle — and &lt;code&gt;handleAsync&lt;/code&gt; is that.&lt;/p&gt;

&lt;p&gt;The next time you evaluate a library, it's worth asking: what problem is this solving, and do I have that problem? Sometimes the answer is no, and the simpler path is already in front of you.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>architecture</category>
      <category>react</category>
    </item>
    <item>
      <title>You Probably Don't Need to Think About UI Optimization: Honest Benchmarks</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 03 Mar 2026 13:30:03 +0000</pubDate>
      <link>https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g</link>
      <guid>https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the third post in a series. &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;Start with the first post&lt;/a&gt; if you haven't already, or &lt;a href="https://dev.to/iceonfire/entity-centric-architecture-a-different-way-to-think-about-web-uis-4mkd"&gt;read the architecture post&lt;/a&gt; for the full mental model.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Framework posts love benchmarks. They also love cherry-picking them. I want to be upfront about what these benchmarks measure, what they don't, and what the numbers actually mean for your daily work.&lt;/p&gt;

&lt;p&gt;All benchmarks are in the &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge/tree/main/benchmarks" rel="noopener noreferrer"&gt;public repo&lt;/a&gt;. Run them yourself on your hardware.&lt;/p&gt;

&lt;p&gt;If you find a flaw in the benchmark methodology, please open an issue or PR. I'd rather fix the benchmark than win an argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;Here's the honest version, up front, instead of buried at the end: &lt;strong&gt;Inglorious Web usually doesn't beat Vue or Svelte, and it doesn't beat Solid at all.&lt;/strong&gt; What it offers instead is comparable performance in these workloads — close enough that users are unlikely to perceive a difference in normal interaction — with a simpler programming model and a testing story that the others don't match. That's the actual pitch of this post. Everything below is the evidence for it, including the parts where Inglorious Web loses.&lt;/p&gt;

&lt;p&gt;Three findings, briefly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On high-frequency bulk updates&lt;/strong&gt; (dashboards, tickers, live data grids), Inglorious Web matches Vue and Svelte's frame rates without any optimization work, in a 4–5x smaller bundle than React. Solid is faster still — its compiler gives it a real, measured scripting-time advantage here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On sparse, deeply nested updates&lt;/strong&gt; (the shape of spreadsheets, conditional forms, dependency chains), fine-grained frameworks — Vue's proxies, Svelte's runes, Solid's signals — all converge on doing almost nothing, while Inglorious Web's full-tree walk costs more. Under realistic usage, that cost is a few milliseconds per update and isn't something a user would notice; under a sustained synthetic stress test, it adds up to something a profiler can see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On developer experience&lt;/strong&gt;, Inglorious Web's event-handler model tests without a runtime, a store, or mocking — call the handler, assert the result. That's the thing none of the other frameworks give you in the same form, and it's the actual differentiator, not the frame rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the deep-dive tables and the "why" behind each number, read on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark 1: The Dashboard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What It Measures
&lt;/h3&gt;

&lt;p&gt;A live-updating dashboard with 1,000 rows of data updating in real time, 10 random rows updated every 10ms (100 updates/second), 4 live charts, filtering and sorting, and an FPS counter. This simulates factory monitoring dashboards, stock tickers, and logistics tracking systems — the kind of UI where high-frequency bulk updates are the norm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fairness Rules
&lt;/h3&gt;

&lt;p&gt;All implementations share the same dataset and update frequency, the same chart model and data slicing, the same business logic shape (event-driven handlers responding to the same event names), the same top-level component structure, and the same CSS.&lt;/p&gt;

&lt;p&gt;One specific thing worth calling out: the four chart components originally rendered their bars using each framework's default list primitive — Vue, Svelte, and React all key their bar lists by index, but Solid's &lt;code&gt;&amp;lt;For&amp;gt;&lt;/code&gt; defaults to keying by item identity/value instead. Since the bar values are plain numbers, that meant Solid's list could skip re-invoking the row function for any bar whose numeric value happened to be unchanged since the last tick — a shortcut the other three don't get with index-based keys. To rule out that this was inflating Solid's numbers, all four Solid variants were switched from &lt;code&gt;&amp;lt;For&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;Index&amp;gt;&lt;/code&gt;, which keys by position exactly like the other frameworks. The numbers below reflect that fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Full Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;FPS (dev)&lt;/th&gt;
&lt;th&gt;FPS (prod)&lt;/th&gt;
&lt;th&gt;Bundle (kB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;113&lt;/td&gt;
&lt;td&gt;62.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Memoized&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;62.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;72.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK Memoized&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;td&gt;72.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK + Inglorious adapters&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;79.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK + Inglorious adapters Memoized&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;79.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + Inglorious Store&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;71.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + Inglorious Store Memoized&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;72.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue&lt;/td&gt;
&lt;td&gt;116&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;td&gt;26.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue + Pinia&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;td&gt;28.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte&lt;/td&gt;
&lt;td&gt;112&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;16.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte + Store&lt;/td&gt;
&lt;td&gt;110&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;16.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte + Runes&lt;/td&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;td&gt;14.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;18.38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid Memoized&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;18.34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid + Store&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;td&gt;23.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid + Store Memoized&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;23.03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inglorious Web&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;118&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;120&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.29&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inglorious Web Memoized&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;115&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;120&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.35&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Measured on MacBook Pro 16" 2023, Chrome 144, macOS Tahoe.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Numbers Mean
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solid is the fastest thing in this benchmark, even after the keying fix.&lt;/strong&gt; It hits 120 FPS in dev and 118–120 in prod with zero optimization work. React's dev/prod gap is the compiler story: React 19.2 ships an automatic compiler that adds memoizations at build time, closing most of the gap manual &lt;code&gt;React.memo&lt;/code&gt;/&lt;code&gt;useMemo&lt;/code&gt; used to require — though the compiler only optimizes patterns it recognizes, so there are still cases where you're back to reasoning about the component tree by hand. The Inglorious adapter layer is the real overhead in the RTK variants (29 FPS dev, 74 FPS prod) — the cost is in the adapter bridging two state models, not in RTK's middleware. Vue, Svelte, and Inglorious Web all stay above 100 FPS in development and reach the monitor refresh limit in production, making them effectively indistinguishable in normal interaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bundle size gap is structural.&lt;/strong&gt; React core alone is 62KB; React + RTK reaches 72KB. Inglorious Web is 16KB total — framework, state manager, and renderer. Vue is 27KB, Svelte is 14–16KB, Solid is 18–23KB. For global audiences on slower connections, that's a meaningful difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Breaking Down the Resource Overhead
&lt;/h3&gt;

&lt;p&gt;FPS numbers show visual smoothness; they don't show how hard the CPU is working. The profiler over a 10-second window fills that in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Main Thread (ms)&lt;/th&gt;
&lt;th&gt;Scripting (ms)&lt;/th&gt;
&lt;th&gt;Rendering (ms)&lt;/th&gt;
&lt;th&gt;JS Heap (MB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;8389&lt;/td&gt;
&lt;td&gt;3596&lt;/td&gt;
&lt;td&gt;5029&lt;/td&gt;
&lt;td&gt;10.4-44.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Memoized&lt;/td&gt;
&lt;td&gt;8345&lt;/td&gt;
&lt;td&gt;1694&lt;/td&gt;
&lt;td&gt;6464&lt;/td&gt;
&lt;td&gt;11.1-33.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK&lt;/td&gt;
&lt;td&gt;8722&lt;/td&gt;
&lt;td&gt;4359&lt;/td&gt;
&lt;td&gt;4539&lt;/td&gt;
&lt;td&gt;21.9-102.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK Memoized&lt;/td&gt;
&lt;td&gt;8666&lt;/td&gt;
&lt;td&gt;4261&lt;/td&gt;
&lt;td&gt;4630&lt;/td&gt;
&lt;td&gt;11.5-45.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK + Inglorious Adapters&lt;/td&gt;
&lt;td&gt;8964&lt;/td&gt;
&lt;td&gt;5466&lt;/td&gt;
&lt;td&gt;3638&lt;/td&gt;
&lt;td&gt;22.0-61.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + RTK + Inglorious Adapters Memoized&lt;/td&gt;
&lt;td&gt;8974&lt;/td&gt;
&lt;td&gt;4355&lt;/td&gt;
&lt;td&gt;4448&lt;/td&gt;
&lt;td&gt;11.8-50.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + Inglorious Store&lt;/td&gt;
&lt;td&gt;8640&lt;/td&gt;
&lt;td&gt;4209&lt;/td&gt;
&lt;td&gt;4612&lt;/td&gt;
&lt;td&gt;10.9-44.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React + Inglorious Store Memoized&lt;/td&gt;
&lt;td&gt;8504&lt;/td&gt;
&lt;td&gt;2241&lt;/td&gt;
&lt;td&gt;6103&lt;/td&gt;
&lt;td&gt;11.5-45.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue&lt;/td&gt;
&lt;td&gt;8645&lt;/td&gt;
&lt;td&gt;2231&lt;/td&gt;
&lt;td&gt;6297&lt;/td&gt;
&lt;td&gt;9.1-40.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue + Pinia&lt;/td&gt;
&lt;td&gt;8608&lt;/td&gt;
&lt;td&gt;2226&lt;/td&gt;
&lt;td&gt;6251&lt;/td&gt;
&lt;td&gt;9.1-38.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte&lt;/td&gt;
&lt;td&gt;8488&lt;/td&gt;
&lt;td&gt;1974&lt;/td&gt;
&lt;td&gt;6412&lt;/td&gt;
&lt;td&gt;6.8-44.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte + Store&lt;/td&gt;
&lt;td&gt;8440&lt;/td&gt;
&lt;td&gt;2026&lt;/td&gt;
&lt;td&gt;6289&lt;/td&gt;
&lt;td&gt;6.8-43.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte + Runes&lt;/td&gt;
&lt;td&gt;8557&lt;/td&gt;
&lt;td&gt;2115&lt;/td&gt;
&lt;td&gt;6312&lt;/td&gt;
&lt;td&gt;8.3-47.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;7764&lt;/td&gt;
&lt;td&gt;531&lt;/td&gt;
&lt;td&gt;6457&lt;/td&gt;
&lt;td&gt;6.5-9.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid Memoized&lt;/td&gt;
&lt;td&gt;8041&lt;/td&gt;
&lt;td&gt;692&lt;/td&gt;
&lt;td&gt;6637&lt;/td&gt;
&lt;td&gt;6.6-10.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid + Store&lt;/td&gt;
&lt;td&gt;8174&lt;/td&gt;
&lt;td&gt;1052&lt;/td&gt;
&lt;td&gt;6498&lt;/td&gt;
&lt;td&gt;8.8-21.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid + Store Memoized&lt;/td&gt;
&lt;td&gt;7870&lt;/td&gt;
&lt;td&gt;747&lt;/td&gt;
&lt;td&gt;6495&lt;/td&gt;
&lt;td&gt;8.4-18.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inglorious Web&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8564&lt;/td&gt;
&lt;td&gt;1924&lt;/td&gt;
&lt;td&gt;6489&lt;/td&gt;
&lt;td&gt;5.2-43.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inglorious Web Memoized&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7463&lt;/td&gt;
&lt;td&gt;1680&lt;/td&gt;
&lt;td&gt;4235&lt;/td&gt;
&lt;td&gt;5.3-36.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Why Solid Wins
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Solid's scripting time is in a different league, and the keying fix barely moved it.&lt;/strong&gt; Switching from &lt;code&gt;&amp;lt;For&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;Index&amp;gt;&lt;/code&gt; — removing the value-based skip-unchanged shortcut, forcing Solid to revisit every bar every tick like everyone else — took scripting time from ~510–530ms to a still-tiny 531–1052ms. That rules out the keying-artifact hypothesis: even keyed identically to the rest, Solid spends a third to a fifth of Svelte's scripting time (~2,000ms).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It's not a bigger cache, it's no cache to check.&lt;/strong&gt; lit-html's caching (what Inglorious Web relies on) re-runs your render function in full every tick, re-evaluating every expression, then skips the DOM &lt;em&gt;write&lt;/em&gt; per part if the value's unchanged — it saves the write, not the recompute. Solid's compiler instead reads your JSX at build time and emits a direct binding per signal — &lt;code&gt;signal.subscribe(v =&amp;gt; node.style.height = v)&lt;/code&gt; — with no template to walk at all. A signal change invokes exactly its one subscriber. That's why Solid's scripting time pulls away while its rendering time (the actual DOM/layout cost) stays in the same range as everyone else's — the gap is in the JS work &lt;em&gt;before&lt;/em&gt; the write, not the write itself. The tradeoff: this requires a compile step over statically analyzable JSX; a runtime loop over arbitrary JS can't be unrolled the same way without one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Could this gap be reduced without adding a compiler? Possibly — an opt-in runtime binding directive is something I'm exploring, but that's future work rather than something measured here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solid's memory footprint follows the same pattern.&lt;/strong&gt; At 6.5–21.9MB, it uses roughly a fifth of Inglorious Web's peak (43.7MB) and Svelte's (49.8MB), because there's no intermediate "all the rows" representation sitting around to diff — updates touch exactly the affected nodes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where Inglorious Web Wins
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Memoized selectors give Inglorious Web its own large win.&lt;/strong&gt; Comparing the unmemoized baseline to &lt;code&gt;Inglorious Web Memoized&lt;/code&gt;, Main Thread time drops over 1,100ms and rendering time falls from 6,489ms to 4,235ms, because the table and charts stop re-computing, sorting, and slicing raw data 100 times a second.&lt;/p&gt;

&lt;h4&gt;
  
  
  What This Means
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Vue and Svelte are efficient in scripting (~2,000ms) but tied to DOM churn&lt;/strong&gt; — without selector memoization for this workload, their rendering times stay pinned at 6,200–6,400ms, similar to Solid's own rendering time. That tells you raw DOM writes, not JS execution, are the shared bottleneck once scripting cost is out of the way. &lt;strong&gt;React pays a heavy rendering tax even with the compiler&lt;/strong&gt; — scripting drops, but rendering time still skyrockets past 6,000ms, since virtual DOM reconciliation and layout passes are unaffected by cutting JS overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put plainly:&lt;/strong&gt; for this workload, Solid's compiled, fine-grained model has a real structural advantage over React's virtual DOM, Vue/Svelte's uncompiled reactivity, and Inglorious Web's full-tree walk — and that holds up after specifically checking for and ruling out one likely confound (list-keying strategy).&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark 2: The Deep Tree
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What It Measures and Why It Matters
&lt;/h3&gt;

&lt;p&gt;A tree of depth 8, branching factor 3, where one random leaf updates every 300ms. React, Vue, Svelte, and Solid all localize the update through their dependency graph — a proxy or signal knows exactly what changed. Inglorious Web walks the whole tree. This is the worst case for the full-tree model, chosen deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;FPS (dev)&lt;/th&gt;
&lt;th&gt;FPS (prod)&lt;/th&gt;
&lt;th&gt;Bundle (kB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;62.16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;25.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;14.68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;15.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inglorious Web&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All five hit the monitor cap in both dev and production. FPS parity is complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the Difference Shows Up
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Main Thread (ms)&lt;/th&gt;
&lt;th&gt;Scripting (ms)&lt;/th&gt;
&lt;th&gt;Rendering (ms)&lt;/th&gt;
&lt;th&gt;JS Heap (MB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;td&gt;23.9–24.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;28.8–29.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte&lt;/td&gt;
&lt;td&gt;206&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;74&lt;/td&gt;
&lt;td&gt;49.3–49.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;107&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;23.6–24.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inglorious Web&lt;/td&gt;
&lt;td&gt;339&lt;/td&gt;
&lt;td&gt;135&lt;/td&gt;
&lt;td&gt;122&lt;/td&gt;
&lt;td&gt;11.4–28.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Inglorious Web uses 135ms of scripting time versus 34–38ms for the other four, over the full 10-second run — with no perceptible effect on FPS, which stays locked at 120 throughout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solid's number here is the interesting one, and it's not what Benchmark 1 predicts.&lt;/strong&gt; At 36ms, Solid lands right next to React, Vue, and Svelte — not in a different league. Its dramatic scripting advantage in Benchmark 1 wasn't "signals are always this much faster" — it was specific to a workload where full-tree walking has real cost to avoid. Here, once an update is localized, there's almost nothing left to skip, so proxies, runes, and compiled signals all converge to roughly the same low cost — the compiler still buys Solid the lowest Main Thread total (107ms vs. 196–251ms), just not the same order-of-magnitude story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why full-tree specifically loses here comes down to scale.&lt;/strong&gt; A depth-8, branching-3 tree has 3⁸ ≈ 6,561 leaf-level nodes; a dependency graph never visits the other 6,560 when one leaf changes, while &lt;code&gt;render()&lt;/code&gt; walks from the root every time because there's no graph telling it what to skip. Bulk updates touch a meaningful fraction of the tree anyway, so walking isn't much extra work there — a single sparse update makes "walk everything" a ~6,561x larger task by comparison, which is the 135ms vs. ~35ms you see above.&lt;/p&gt;

&lt;h3&gt;
  
  
  When It Would Matter
&lt;/h3&gt;

&lt;p&gt;Worth being precise about what this benchmark simulates: one update every 300ms, sustained continuously for 10 seconds — about 33 updates back to back with no pause. That's a deliberately adversarial, synthetic pattern, not how sparse updates arrive in a real app. A person expanding tree nodes does it in short bursts and stops; nothing about normal interaction sustains a continuous multi-times-per-second cadence for ten seconds straight. And even under that adversarial pattern, the per-update cost is small: 135ms over ~33 updates is about 4ms each — under a single 60fps frame budget, let alone 120fps.&lt;/p&gt;

&lt;p&gt;So the honest conclusion isn't "full-tree rendering has a weak spot to watch out for." It's: &lt;strong&gt;this is a deliberately worst-case synthetic stress test, built to surface a cost, and even there the per-update cost is imperceptible.&lt;/strong&gt; If something genuinely does sustain rapid, continuous, sparse updates — a live data feed driving a deep hierarchy, not occasional user clicks — the fine-grained frameworks' converging advantage (34–38ms vs. 135ms in total) is real and worth knowing about. For the far more common case of occasional, user-driven sparse updates, the difference won't be felt.&lt;/p&gt;

&lt;p&gt;Put both benchmarks together: &lt;strong&gt;for bulk updates, full-tree rendering costs about the same scripting time as proxies and runes, and only compiled signals buy you meaningfully more. For sustained, rapid sparse updates, full-tree rendering costs more in aggregate — proxies, runes, and signals converge — though the per-update cost in both cases is small enough that it mostly matters for continuous, machine-driven update streams rather than everyday interaction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inglorious Web's case isn't that full-tree rendering is faster in either scenario. It's that the mental model stays simple regardless of workload shape: plain event handlers, no signal graph, no compiler step, and (see Testing, below) a simple story for writing and running tests. This fits real-time dashboards, financial tickers, IoT monitoring panels, and collaborative/multiplayer state sync particularly well — &lt;code&gt;@inglorious/server&lt;/code&gt; uses the same event system over WebSockets, so adding real-time collaboration to an existing app is a middleware line, not an architectural change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing: The Developer Performance Benchmark
&lt;/h2&gt;

&lt;p&gt;Runtime benchmarks measure one dimension. Developer performance — how fast you can write, change, and verify code — matters at least as much over the life of a project.&lt;/p&gt;

&lt;h3&gt;
  
  
  React Hooks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;act&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useTaskList&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write documentation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need &lt;code&gt;@testing-library/react&lt;/code&gt;, &lt;code&gt;renderHook&lt;/code&gt;, and &lt;code&gt;act()&lt;/code&gt; wrappers, and the React runtime. The ceremony is high enough that many teams skip unit-testing hooks entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  React + Redux
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tasksReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;taskAdded&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./taskSlice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tasksReducer&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="nf"&gt;taskAdded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write documentation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redux reducers test in complete isolation — no store setup, no runtime. This is genuinely good, and it's the same instinct behind Inglorious Web's handler design. Where it gets harder is async: testing a thunk requires mocking &lt;code&gt;dispatch&lt;/code&gt;, &lt;code&gt;getState&lt;/code&gt;, and side effects, and the setup grows with complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inglorious Web
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inglorious/web/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;TaskList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskAdd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Write documentation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No runtime. No store setup. No action creators. Call the handler, assert the result — and because handlers can be async and notify further events, the same pattern extends to async: &lt;code&gt;trigger&lt;/code&gt; returns a promise when the handler is async, and the &lt;code&gt;events&lt;/code&gt; array captures any notifications fired during it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;React&lt;/th&gt;
&lt;th&gt;Vue&lt;/th&gt;
&lt;th&gt;Svelte&lt;/th&gt;
&lt;th&gt;Solid&lt;/th&gt;
&lt;th&gt;Inglorious Web&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dev FPS (bulk updates)&lt;/td&gt;
&lt;td&gt;32–112 (optimization required)&lt;/td&gt;
&lt;td&gt;116–117&lt;/td&gt;
&lt;td&gt;102–112&lt;/td&gt;
&lt;td&gt;119–120&lt;/td&gt;
&lt;td&gt;105–118&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prod FPS (bulk updates)&lt;/td&gt;
&lt;td&gt;92–120 (optimization required)&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;td&gt;118–119&lt;/td&gt;
&lt;td&gt;118–120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FPS (sparse deep tree)&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripting overhead (bulk updates)&lt;/td&gt;
&lt;td&gt;Medium–High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripting overhead (sparse updates)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low (converges with others)&lt;/td&gt;
&lt;td&gt;Higher in aggregate, still &amp;lt;1 frame per update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory footprint (bulk updates)&lt;/td&gt;
&lt;td&gt;Medium–High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Medium–High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;62–79KB&lt;/td&gt;
&lt;td&gt;27–29KB&lt;/td&gt;
&lt;td&gt;14–16KB&lt;/td&gt;
&lt;td&gt;18.3–23KB&lt;/td&gt;
&lt;td&gt;16KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimization required&lt;/td&gt;
&lt;td&gt;Not in prod (compiler); yes in dev&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing complexity&lt;/td&gt;
&lt;td&gt;High (hooks) / Low (reducers) / Hard (async thunks)&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three tiers fall out of the data. &lt;strong&gt;Svelte, Vue, and Inglorious Web are all sufficiently fast for the large majority of applications&lt;/strong&gt; — 116–120 FPS across bulk updates, 120 FPS on the sparse deep tree, none of the three ever close to a visibly choppy frame rate. Within this tier, the differentiator isn't speed; it's what else you get, and for Inglorious Web that's the testing story above. &lt;strong&gt;Solid is the right choice specifically for cascading dependency chains and resource-constrained devices&lt;/strong&gt; — spreadsheet-shaped problems and constrained hardware, where its lowest-in-class scripting time and memory footprint are decisive rather than incidental. &lt;strong&gt;Full-tree rendering's cost shows up only under sustained, rapid, sparse updates&lt;/strong&gt;, and mostly matters for continuous, machine-driven streams rather than everyday clicking; under bulk updates it costs about the same as proxies and runes, with only compiled signals pulling meaningfully ahead. For teams coming from React and Redux, the migration is incremental — Redux DevTools still work — and the testing story holds regardless of which tier your workload falls into.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inglorious Web isn't trying to win the performance benchmark.&lt;/strong&gt; It's trying to be fast enough that performance stops being the deciding factor, so the decision comes down to what you actually spend your time on day to day — reasoning about state, writing tests, and debugging — where a plain event-driven model offers a simpler testing and reasoning story than hooks, signal graphs, or reactive proxies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix: Further Notes
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The following two points came up during review. They're worth recording, but they're a different kind of claim than the benchmarks above — one is a narrower, less rigorous comparison, and the other is architectural philosophy rather than a measured result. Both are flagged as such.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on chart rendering.&lt;/strong&gt; An earlier draft of this post compared &lt;code&gt;@inglorious/charts&lt;/code&gt; against Recharts and found Inglorious's charts hitting 115–120 FPS versus Recharts' 85–95 FPS. That comparison is weaker than Benchmark 1 or 2: Recharts and &lt;code&gt;@inglorious/charts&lt;/code&gt; are chart &lt;em&gt;libraries&lt;/em&gt;, not equivalent implementations of the same spec in different frameworks, so the gap could reflect library design as much as framework architecture. A fair version of this comparison — same chart spec, implemented natively across frameworks — is a big enough undertaking that it probably deserves its own follow-up post rather than a paragraph here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on game engines and ECS.&lt;/strong&gt; Inglorious Web's entity-centric model is drawn from game development, and the sparse-vs-bulk split in this post maps onto that origin: game engines don't reach for signals for their simulation core, because physics, animation, and AI tick nearly every active entity every frame — a bulk-update workload where almost nothing is stable frame-to-frame for a signal graph to skip. ECS's actual payoff there is cache locality and parallelism, not localized change tracking. Signals do show up inside games, just not in the simulation loop — HUDs and menus are sparse-update problems, structurally similar to Benchmark 2. This is a reasonable architectural parallel, not a benchmarked claim, and it's worth reading as color rather than evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;Performance is one side of the picture. The other is server state — how you fetch data, handle loading and error states, and keep your UI in sync with the backend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/iceonfire/you-might-not-need-tanstack-query-2f3l"&gt;The next post&lt;/a&gt; looks at how the entity-centric model interacts with async data fetching, why &lt;code&gt;handleAsync&lt;/code&gt; covers most of what TanStack Query provides, and when you'd still want a dedicated data-fetching library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>architecture</category>
      <category>frontend</category>
    </item>
    <item>
      <title>What If Components Aren’t the Unit of Composition?</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 24 Feb 2026 13:30:03 +0000</pubDate>
      <link>https://dev.to/iceonfire/entity-centric-architecture-a-different-way-to-think-about-web-uis-4mkd</link>
      <guid>https://dev.to/iceonfire/entity-centric-architecture-a-different-way-to-think-about-web-uis-4mkd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the second post in a series. &lt;a href="https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988"&gt;Start with the first post&lt;/a&gt; if you haven't already.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the first post I argued that the real problem with modern frontend frameworks isn't performance or verbosity — it's locality of reasoning. You can't look at a line of code and know when or why it will execute. I also laid out the case against signals, composables, and runes specifically: they shift the problem rather than solve it, trading one invisible system for another.&lt;/p&gt;

&lt;p&gt;This post is about what the alternative actually looks like in practice — how the entity-centric model holds up beyond a counter example, where it fits, and where it honestly doesn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Component-Centric Assumption
&lt;/h2&gt;

&lt;p&gt;Every mainstream framework is built on the same foundational assumption: &lt;strong&gt;the component is the unit of composition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Logic lives in components. State is owned by components or lifted up from them. The UI is a tree of components, each responsible for its own piece of the world.&lt;/p&gt;

&lt;p&gt;This model works well for many applications, especially when UI structure and domain structure align closely. But as complexity grows — particularly in data-heavy tools and dashboards — a structural tension can appear: &lt;strong&gt;your business logic becomes entangled with your rendering tree&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;State that should logically be global ends up local. Behaviors that should be composable get scattered across components. And when you need multiple independent instances of the same domain concept — say, four charts on a dashboard — you can find yourself working around the component hierarchy rather than describing your domain directly.&lt;/p&gt;

&lt;p&gt;Framework ecosystems have evolved good solutions to mitigate this. Vue's composables and Pinia are excellent examples, and React applications often rely on external state managers. These tools solve real problems.&lt;/p&gt;

&lt;p&gt;But they still operate &lt;strong&gt;within the component-centric model&lt;/strong&gt;: components remain the primary organizing structure, and reactive machinery determines when logic runs.&lt;/p&gt;

&lt;p&gt;The question isn't whether these solutions improve ergonomics. It's whether &lt;strong&gt;the component tree is the right abstraction for modeling application behavior in the first place&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Entity-Centric Model
&lt;/h2&gt;

&lt;p&gt;Inglorious Web starts from a different assumption: &lt;strong&gt;entities are the unit of composition, and some entities happen to render.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your application is a flat collection of entities — plain JavaScript objects with an &lt;code&gt;id&lt;/code&gt; and a &lt;code&gt;type&lt;/code&gt;. Types define behavior: they're objects whose methods are event handlers.&lt;/p&gt;

&lt;p&gt;Most types have a &lt;code&gt;render&lt;/code&gt; method, because this is a web UI framework after all. But &lt;code&gt;render&lt;/code&gt; is not privileged — it's just another method on the type, and the store doesn't care about it. Some types genuinely don't render at all: a session manager, a WebSocket handler, a background polling loop. They live alongside rendering entities in the same store and respond to events the same way.&lt;/p&gt;

&lt;p&gt;The architecture becomes clearer when visualized:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbznh0qk11t4jdxb1ny2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbznh0qk11t4jdxb1ny2.png" alt="Event Queue" width="646" height="801"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Architecture overview: user actions emit events through &lt;code&gt;api.notify()&lt;/code&gt;.&lt;br&gt;
Events are processed deterministically through a queue, dispatched to entity handlers, which update state and trigger rendering.&lt;br&gt;
The event timeline and state snapshots can be inspected using &lt;a href="https://github.com/reduxjs/redux-devtools" rel="noopener noreferrer"&gt;Redux DevTools&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The simplest setup uses &lt;code&gt;autoCreateEntities&lt;/code&gt;, which creates one entity per type automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;TaskList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nf"&gt;taskAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nf"&gt;taskToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
        &amp;lt;ul&amp;gt;
          &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
              &amp;lt;li
                class=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
                @click=&lt;/span&gt;&lt;span class="p"&gt;${()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:taskToggle`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
              &amp;gt;
                &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
              &amp;lt;/li&amp;gt;
            `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
        &amp;lt;/ul&amp;gt;
      `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;autoCreateEntities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you need multiple instances of the same type — the four-charts case — you declare them explicitly instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;chart1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;chart2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;chart3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;chart4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entities&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each instance gets its own state, managed independently, without lifting state or threading context through component trees.&lt;/p&gt;

&lt;p&gt;Notice what's absent from the type definition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no &lt;code&gt;useState&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;no lifecycle hooks&lt;/li&gt;
&lt;li&gt;no props&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entity holds the data. The type holds the behavior.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;create&lt;/code&gt; and &lt;code&gt;render&lt;/code&gt; live in the same object not because the framework requires it, but because they describe the same entity. You could split them across files and nothing would break — the co-location is for readability, not framework constraints.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Testing Gets Simple
&lt;/h2&gt;

&lt;p&gt;Event handlers are plain functions. Testing them requires no DOM, no component tree, and no lifecycle setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inglorious/web/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;taskList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskAdd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write documentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write documentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendering is equally direct because &lt;code&gt;render&lt;/code&gt; is a pure function of the entity state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inglorious/web/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TaskList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write documentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Write documentation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This style of testing isn't unique — Redux reducers and well-structured composables can also be tested this way. The difference here is that &lt;strong&gt;this is the default architecture of the framework&lt;/strong&gt;, not something achieved through discipline or additional patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Type Composition: Behavior Without Inheritance
&lt;/h2&gt;

&lt;p&gt;Types are plain objects and compose naturally as arrays. Each entry in the array can be either a behavior object or a decorator function that receives the type composed so far.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Timestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SoftDeletable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;softDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deletedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;TaskList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;TaskList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Timestamps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SoftDeletable&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decorator functions allow wrapping existing behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withLogging&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rendering &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;TaskList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;taskList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withLogging&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern handles cross-cutting concerns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;analytics&lt;/li&gt;
&lt;li&gt;route guards&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without inheritance hierarchies or higher-order components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-Entity Communication
&lt;/h2&gt;

&lt;p&gt;Entities communicate through events.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;api.notify()&lt;/code&gt; call supports three targeting modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;save&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;// broadcast&lt;/span&gt;
&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chart:refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// type-targeted&lt;/span&gt;
&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#chart1:refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// id-targeted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handlers can emit further events, and all events pass through a queue that processes them in order. This guarantees deterministic execution regardless of how many entities interact.&lt;/p&gt;

&lt;p&gt;A common concern with event-driven architectures is that event flows can become difficult to trace. Inglorious Web addresses this by integrating with &lt;strong&gt;Redux DevTools&lt;/strong&gt;, which provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a full event log&lt;/li&gt;
&lt;li&gt;state snapshots&lt;/li&gt;
&lt;li&gt;time-travel debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every &lt;code&gt;notify()&lt;/code&gt; call appears in the DevTools timeline, so you can inspect exactly which events fired, in what order, and how the state changed after each one. This makes the event graph visible rather than implicit.&lt;/p&gt;

&lt;p&gt;If a render function needs to read another entity's state, &lt;code&gt;api.getEntity()&lt;/code&gt; provides a read-only snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mainTable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`Showing &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filteredRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; rows`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice this means that when something behaves unexpectedly, you can either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search the codebase for the event name, or&lt;/li&gt;
&lt;li&gt;inspect the event log in DevTools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, the dependency graph lives in your code and tooling rather than inside a reactive runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Entity Model Is Not
&lt;/h2&gt;

&lt;p&gt;A few clarifications that come up often:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's not strict ECS.&lt;/strong&gt;&lt;br&gt;
Game-engine ECS separates entities, components, and systems into distinct concepts optimized for thousands of homogeneous objects updated every frame. Inglorious Web collapses those concepts into types that carry both data shape and behavior, which better fits typical web UI workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's not a universal replacement for existing frameworks.&lt;/strong&gt;&lt;br&gt;
If you're heavily invested in an ecosystem like React or building animation-heavy interfaces, switching architectures has real costs. The entity model targets a specific class of problems: predictable state, debuggable event flows, and complex data-driven interfaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The render model is a deliberate tradeoff.&lt;/strong&gt;&lt;br&gt;
When state changes, the whole tree re-renders and the DOM diff is handled by lit-html. It could seem like naïve re-rendering, but it's not. Fine-grained reactivity systems update only the nodes that changed, which can reduce CPU usage but often increase memory pressure and complexity. Lit-html, in contrast, performs surgical DOM updates while keeping allocations low — deliberately trading fine-grained tracking for a simpler mental model. For many dashboards and data tools the difference is negligible, and reasoning about the UI becomes easier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyuec3vroch64i3khfbi6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyuec3vroch64i3khfbi6.png" alt="Full Tree Re-render" width="630" height="907"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How updates propagate.&lt;/strong&gt;&lt;br&gt;
In reactive component frameworks such as React and Vue, state changes pass through a reactive dependency graph that determines which components re-run.&lt;/p&gt;

&lt;p&gt;In the entity-centric model, events update the entity store and trigger a single root render, making the UI tree a pure function of the current state.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Ecosystem
&lt;/h2&gt;

&lt;p&gt;Inglorious Web ships with built-ins that cover the most common needs: a Redux-compatible state manager with DevTools support, lit-html rendering, form handling, table, virtualized list, select, router, declarative SVG charts, an animation library, and SSG with HMR, file-based routing, and Markdown. Because it renders to the real DOM, it works with any web component out of the box — Shoelace, Material Web, Spectrum — no adapter, no wrapper.&lt;/p&gt;

&lt;p&gt;The honest gaps: the third-party ecosystem is younger than React's. Event name strings in &lt;code&gt;api.notify()&lt;/code&gt; are not yet type-checked. The animation library covers common cases but not everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;Inglorious Web is for teams who want their state to be predictable, their tests to be trivial, and their render logic to be legible. It scales from a todo list to a config-driven dashboard precisely because the mental model doesn't change as complexity grows — you keep adding entities and types, not new abstractions.&lt;/p&gt;

&lt;p&gt;It resonates strongly with Redux veterans (the &lt;a href="https://redux.js.org/understanding/thinking-in-redux/three-principles" rel="noopener noreferrer"&gt;three principles&lt;/a&gt; are intact, the boilerplate is gone), developers who've worked with ECS in game engines, and engineers building dashboards, HMIs, and data-heavy tools where the component tree metaphor starts feeling forced. It's a harder sell for teams that prioritize deep React ecosystem integration or prefer declarative reactivity as a philosophical stance.&lt;/p&gt;

&lt;p&gt;The learning curve is real — not because the framework is complex, but because thinking in entities and events rather than components and hooks is a genuine shift. Most developers who've used Redux and felt "this is almost right" pick it up quickly. For everyone else, it's a one-time investment rather than a continuous tax.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;In the &lt;strong&gt;&lt;a href="https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g"&gt;third post&lt;/a&gt;&lt;/strong&gt;, I'll show the numbers: a benchmark running 1000 rows at 100 updates per second across React (naive, memoized, and with RTK) and Inglorious Web (with and without memoization), and a live chart benchmark against Recharts. Performance, bundle size, and what "dramatically smaller optimization surface area" actually looks like when measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I'm Done With Magic. Here's What I Built Instead.</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Tue, 17 Feb 2026 13:30:04 +0000</pubDate>
      <link>https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988</link>
      <guid>https://dev.to/iceonfire/im-done-with-magic-heres-what-i-built-instead-988</guid>
      <description>&lt;p&gt;The JavaScript ecosystem has a magic problem.&lt;/p&gt;

&lt;p&gt;Not the fun kind. The kind where you stare at your code, everything looks correct, and something still breaks in a way you can't explain. The kind where you spend forty minutes debugging why your &lt;code&gt;computed()&lt;/code&gt; stopped updating, or why an effect fired when you didn't expect it, or why destructuring a store value makes it stop being reactive.&lt;/p&gt;

&lt;p&gt;We called it reactivity. We called it signals. We called it runes. And every new name comes with a new layer of invisible machinery running underneath your code, doing things you didn't ask for, breaking in ways you didn't anticipate. The deeper problem isn't performance or verbosity — it's locality of reasoning. You can't look at a line of code and know when or why it will execute.&lt;/p&gt;

&lt;p&gt;I've been building complex web applications for eighteen years — interactive dashboarding systems, industrial HMI interfaces, config-driven UIs. Those projects are the reason this framework exists: not because they failed, but because I could see exactly how they would age. I got tired of the magic. So I built something without it.&lt;/p&gt;

&lt;p&gt;This isn't the Nth framework built out of frustration. It's a deliberate synthesis of ideas that already proved themselves: Redux's three principles, the Entity Component System architecture from game engines, and lit-html's surgical DOM updates. None of these are new. What's new is putting them together and following the logic all the way through.&lt;/p&gt;




&lt;h2&gt;
  
  
  Once Upon A Time, There Were Three Principles
&lt;/h2&gt;

&lt;p&gt;I really started digging React when they introduced Redux. It revamped Functional Programming concepts as good practices for large-scale systems — proving they belonged in production code, not just CS theory. Three principles made any webapp predictable, debuggable, and testable as never before:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single Source Of Truth:&lt;/strong&gt; one state to rule them all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Is Read-Only:&lt;/strong&gt; reference comparisons make re-render decisions trivial and performant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes Through Pure Functions:&lt;/strong&gt; reducers make logic trivial to reason about.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But things went south. Devs complained that Redux was too verbose, immutable updates were painful, async logic was a hack. Those complaints were valid — the boilerplate was a genuine tax. Enter RTK, which solved real problems: simpler reducers, built-in Immer, sane async thunks. But then it kept going — &lt;code&gt;createAppSlice&lt;/code&gt;, builder callback notation, circular dependency nightmares. The question isn't whether Redux needed fixing. It's whether the fixes took things in the right direction. Then the "Single Source Of Truth" dogma started bending entirely: local state here, Context there, Zustand, Jotai, signals. We write less code now, and it just magically works. Well — not for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Magic
&lt;/h2&gt;

&lt;p&gt;Let me be specific, because "magic is bad" is an easy claim to make and a hard one to defend without evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React&lt;/strong&gt; re-renders are actually fast — React was right about that. The real problem is that re-renders trigger effects and lifecycle methods. &lt;code&gt;useEffect&lt;/code&gt; fires after every matching render, subscriptions re-initialize, derived state recomputes. Invisible dependency arrays silently break when you forget something, and &lt;code&gt;useEffect&lt;/code&gt; lists grow into things nobody on the team fully trusts. React's answer? A stable compiler that adds layers of cache automatically. Which means you can have a suboptimal component hierarchy and the compiler will compensate — which is convenient until you need to understand why something broke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vue 3&lt;/strong&gt; introduced a subtle trap with the Composition API: destructuring a reactive object silently breaks the proxy chain that powers reactivity. Your variable stops updating and you get no warning whatsoever. Vue provides &lt;code&gt;toRefs()&lt;/code&gt; specifically to patch this — which proves the point: you now have to manage the integrity of an invisible system on top of writing your actual application. And &lt;code&gt;computed()&lt;/code&gt; knows when to recompute by secretly tracking which reactive properties you accessed while it ran, which can produce circular dependencies that only blow up at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Svelte 5&lt;/strong&gt; introduced runes — &lt;code&gt;$state()&lt;/code&gt;, &lt;code&gt;$derived()&lt;/code&gt;, &lt;code&gt;$effect()&lt;/code&gt;. The docs themselves define the word:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;rune /ruːn/ noun — A letter or mark used as a mystical or magic symbol.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's impressive engineering. But unlike JSX — which is a purely syntactic transformation — Svelte's compiler is &lt;em&gt;semantically active&lt;/em&gt;: it changes what your code means, not just how it looks. &lt;code&gt;$state()&lt;/code&gt; isn't JavaScript with nicer syntax; it's a different programming model that requires the compiler to be correct.&lt;/p&gt;

&lt;p&gt;All three are racing in the same direction: more reactivity, more compilation, more invisible machinery. I went the other way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Boring Alternative
&lt;/h2&gt;

&lt;p&gt;Inglorious Web is built on one idea: &lt;strong&gt;state is data, behavior are functions, rendering is a pure function of state.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No proxies. No signals. No compiler. Just plain JavaScript objects, event handlers, and lit-html's surgical DOM updates. The mental model is a one-time cost, not a continuous tax — you learn it once, and it scales without adding new concepts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;div&amp;gt;
        &amp;lt;span&amp;gt;Count: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/span&amp;gt;
        &amp;lt;button @click=&lt;/span&gt;&lt;span class="p"&gt;${()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:increment`&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;
          +1
        &amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks like a hybrid between Vue's Options API and React's JSX. If you prefer either of those syntaxes, there are Vite plugins for both. But the key differences are in what's &lt;em&gt;absent&lt;/em&gt;. There are no hooks, no lifecycle methods, no component-level state. &lt;code&gt;create&lt;/code&gt; and &lt;code&gt;increment&lt;/code&gt; are plain event handlers — closer to RTK reducers than to React methods. The templates are plain JavaScript tagged literals: no new syntax to learn, no compilation step required. Boring doesn't mean verbose — it means every line does exactly what it says.&lt;/p&gt;

&lt;p&gt;One deliberate abstraction worth naming: state mutations inside handlers look impure but aren't. The framework wraps them in &lt;a href="https://mutative.js.org/" rel="noopener noreferrer"&gt;Mutative&lt;/a&gt; — the same structural sharing idea as Immer, but 2–6x faster — so you write &lt;code&gt;entity.value++&lt;/code&gt; and get back an immutable snapshot. That's the only &lt;em&gt;reactive&lt;/em&gt; magic in the stack, it's a small and well-understood library, and it's what makes testing trivial.&lt;/p&gt;

&lt;p&gt;When state changes, the whole tree re-renders. But lit-html only touches the DOM nodes that actually changed — the same way Redux reducers don't do anything when an action isn't their concern. Re-rendering is cheap. Effects and lifecycle surprises don't exist. The question "why did this effect fire?" is simply impossible to ask, because you can look at any handler and reason about exactly when it runs. And because every state transition is an explicit event, you can grep for every place it's fired — something you cannot do with a reactive dependency graph.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing That Actually Makes Sense
&lt;/h2&gt;

&lt;p&gt;In React, testing a component with hooks means setting up a fake component tree and mocking the world around it. In Vue 3, testing a composable means testing impure functions swimming in proxy magic.&lt;/p&gt;

&lt;p&gt;In Inglorious Web, testing state logic is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trigger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inglorious/web/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And testing rendering is equally straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@inglorious/web/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Count: 42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// snapshot testing works too:&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchSnapshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No fake component tree. No lifecycle setup. No async ceremony. Because render is a pure function of an entity, and a pure function is just a function you call.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mental Model Shift
&lt;/h2&gt;

&lt;p&gt;React, Vue, and Svelte are component-centric. The component is the unit. Logic lives in components, state is owned or lifted by them, everything is a tree.&lt;/p&gt;

&lt;p&gt;Inglorious Web is entity-centric. Your application is a collection of entities — pieces of state with associated behaviors. Some entities happen to render. Most of the time you don't think about the tree at all.&lt;/p&gt;

&lt;p&gt;If you've heard of the Entity Component System (ECS) architecture used in game engines, this will feel familiar — though it's not a strict implementation. Think of it as ECS meets Redux: entities hold data, types hold behavior, and the store is the single source of truth. The practical consequence is that you can add, remove, or compose behaviors at the type level without touching the UI, and you can test state logic in complete isolation from rendering. That's not just less magic — it's a different ontology.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;This is the first post in a series.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;&lt;a href="https://dev.to/iceonfire/entity-centric-architecture-a-different-way-to-think-about-web-uis-4mkd"&gt;next post&lt;/a&gt;&lt;/strong&gt;, I'll go deeper into the entity-centric architecture: how types compose, how the ECS lineage maps to real web UI problems, and whether the mental model holds up at scale — from a TodoMVC to a config-driven industrial HMI. I'll also be honest about the ecosystem, the tradeoffs, and where the framework fits and where it doesn't.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;&lt;a href="https://dev.to/iceonfire/you-probably-dont-need-to-think-about-ui-optimization-honest-benchmarks-2m5g"&gt;third post&lt;/a&gt;&lt;/strong&gt;, I'll show the numbers: a benchmark running 1000 rows at 100 updates per second, comparing React (naive, memoized, and with RTK), and a live chart benchmark against Recharts. Performance, bundle size, and what "dramatically smaller optimization surface area" actually looks like in practice.&lt;/p&gt;

&lt;p&gt;The ecosystem is moving toward more magic. I'm moving the other way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://inglorious.dev/web/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/IngloriousCoderz/inglorious-forge" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>redux</category>
    </item>
  </channel>
</rss>
