<?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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3571361%2F48e606a1-5728-42f5-9d0b-cf2e685082e8.jpeg</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>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 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.&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;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.&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="347"&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;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 Inglorious Web. 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;, 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;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;.&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;&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;The short version: &lt;strong&gt;Inglorious Web matches the fastest alternatives without any optimization work, in a bundle that's 4–5x smaller than React.&lt;/strong&gt; Vue and Svelte are equally fast — that's not a surprise, and it's not a problem. The real differentiator isn't raw speed. It's that Inglorious Web's optimization surface area is dramatically smaller. You write naive code and get optimal performance. But there's a scenario where full-tree rendering has a measurable cost, and I'll show you that too — along with why it almost certainly doesn't matter for your users.&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;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. This is a genuine apples-to-apples comparison.&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;&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;Vue and Svelte are fast. That's expected and honest.&lt;/strong&gt; Both reach comparable FPS to Inglorious Web in development and match it in production. Raw rendering speed is not where Inglorious Web differentiates — and this post won't pretend otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The React dev/prod gap is the compiler story.&lt;/strong&gt; These benchmarks use React 19.2, which ships with an automatic compiler. The naive React variant drops to 52 FPS in development but recovers to 113 FPS in production — because the compiler adds memoizations automatically at build time. Manual &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; are no longer required for production performance. The compiler recovers performance that would previously require manual optimization work. The underlying rendering model remains the same, though — components can still re-render broadly, and the compiler only optimizes patterns it can safely recognize. When it encounters a pattern it can't, you're back to reasoning about the component tree manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Inglorious adapter layer is the real overhead in RTK variants.&lt;/strong&gt; React + RTK on its own (32 FPS dev, 92 FPS prod) isn't slow because of RTK's middleware — it's slow because of how RTK interacts with React's rendering model in this workload. The variants using Inglorious adapters on top of RTK perform worse still (29 FPS dev, 74 FPS prod), which tells you the cost is in the adapter layer bridging two state models, not in RTK's middleware itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inglorious Web is fast by default and gains little from memoization.&lt;/strong&gt; The baseline variant (105 FPS dev, 120 prod) already matches the best alternatives. Adding &lt;code&gt;compute()&lt;/code&gt; for derived state brings it to 110 FPS dev — a modest gain, confirming that the baseline update model is already efficient. You don't need to think about optimization, and there's no compiler pass silently doing it for you.&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 16KB. For global audiences on slower connections, this matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark 2: The Charts
&lt;/h2&gt;

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

&lt;p&gt;Four live-updating charts — line, area, bar, pie — with 100 data points each, updated at 10 updates/second. Compared against Recharts, the most widely used React chart library.&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;Implementation&lt;/th&gt;
&lt;th&gt;FPS (120Hz monitor)&lt;/th&gt;
&lt;th&gt;vs Recharts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;📊 Recharts&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;85–95 FPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚀 Inglorious Charts (Config)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;115–120 FPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+35% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🚀 Inglorious Charts (Composition)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;115–120 FPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+35% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;On 60Hz monitors: Inglorious ~60 FPS, Recharts ~51 FPS.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This one Inglorious Web wins clearly. Recharts carries React's rendering overhead. &lt;code&gt;@inglorious/charts&lt;/code&gt; is built on the same entity-centric model — charts are entities, lit-html handles surgical DOM updates, no virtual DOM reconciliation. For data-dense dashboards with multiple live charts, the gap is meaningful.&lt;/p&gt;




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

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

&lt;p&gt;This benchmark is specifically designed to challenge full-tree rendering: a tree of depth 8 with a branching factor of 3, where one random leaf updates every 300ms.&lt;/p&gt;

&lt;p&gt;React, Vue, and Svelte handle this with fine-grained reactivity — only the affected leaf rerenders. 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;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 four 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;p&gt;FPS isn't the whole story. Here's what the profiler shows in production over a 10-second window:&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;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;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 ~35ms for the others. That's about 100ms more — over 10 seconds. That's 1% of wall-clock time, on a workload specifically engineered to be the worst case for full-tree rendering, with no perceptible effect on the user and all variants locked at 120 FPS throughout.&lt;/p&gt;

&lt;p&gt;It's a real cost. The profiler can measure it. But it's not a practical concern for any real application — and it comes with a tradeoff in the other direction: Inglorious Web's JS heap (11.4–28.1 MB) is substantially lower than Svelte's (49.3–49.8 MB). Full-tree rendering trades a small amount of CPU time for lower memory pressure. Whether that's a good trade depends on your constraints.&lt;/p&gt;

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

&lt;p&gt;If you're building a deeply nested UI where individual nodes update rarely and in isolation, and CPU efficiency is a hard requirement, fine-grained reactive frameworks have a genuine edge. Inglorious Web's primary workload is the opposite — high-frequency bulk updates across many entities simultaneously. Workloads that fit this model well include real-time dashboards, financial tickers, IoT monitoring panels, collaborative editing tools, and multiplayer state synchronization. The last two are worth noting specifically: &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 single middleware line rather than an architectural change. The deep tree benchmark is a useful stress test of the tradeoff, not a representative scenario for any of these.&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. Testing is where architectural choices become visceral.&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. The React runtime is required. 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 are pure functions and test in complete isolation — no store setup, no runtime needed. 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 any side effects, and the setup grows quickly 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. Just call the handler, assert the result. And because handlers can be async and can notify further events, the same pattern works for async too — &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 the handler. There's nothing else to learn.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Summary
&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;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;105–110&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;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;/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;~1% higher&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;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;/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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The right framework depends on your workload and priorities. For high-frequency bulk updates, data-dense dashboards, and event-driven UIs, Inglorious Web performs optimally without any optimization work. For deeply nested UIs with sparse, localized updates and strict CPU budgets, fine-grained reactive frameworks have a measurable efficiency edge. For teams coming from React and Redux, the mental model transfer is natural and the Redux DevTools compatibility makes migration incremental.&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;The next post 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. The short version: if you have a centralized store, you may not have the problem TanStack Query was built to solve.&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>
    <item>
      <title>Inglorious Store: a Redux-compatible state manager inspired by ECS</title>
      <dc:creator>Matteo Antony Mistretta</dc:creator>
      <pubDate>Fri, 17 Oct 2025 16:58:56 +0000</pubDate>
      <link>https://dev.to/iceonfire/inglorious-store-a-redux-compatible-state-manager-inspired-by-ecs-58j0</link>
      <guid>https://dev.to/iceonfire/inglorious-store-a-redux-compatible-state-manager-inspired-by-ecs-58j0</guid>
      <description>&lt;p&gt;&lt;strong&gt;I love Redux&lt;/strong&gt;. It taught me how cool functional programming can be. For a long time I even wondered if using functional programming for games could be a good fit. Some examples proved that you can indeed program games in Haskell, Lisp, or F#. But, is it worth it?&lt;/p&gt;

&lt;p&gt;A few years ago I had an idea: create my own version of Redux, but tailored for videogames. Thus the &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@inglorious/engine" rel="noopener noreferrer"&gt;Inglorious Engine&lt;/a&gt;&lt;/strong&gt; was born. The proof-of-concept was solid, but then other things got in the way and I abandoned it for quite some time.&lt;/p&gt;

&lt;p&gt;Last summer I was asked to speak at a conference, so I brought the engine back. While re-reading its code I said to myself: "You know what? This isn't half bad!" So I went on evolving it. I even created a new superset of JavaScript: &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; adds vector operations natively to the language, making game logic even simpler.&lt;/p&gt;

&lt;p&gt;Then I went back to the state management component, and I noticed something: this can be used for apps too! It could have been a life-saver that time I had to build a React framework for interactive dashboards.&lt;/p&gt;

&lt;p&gt;Long story short, this is how the &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@inglorious/store" rel="noopener noreferrer"&gt;Inglorious Store&lt;/a&gt;&lt;/strong&gt; was born. To me, it's like Redux should be rewritten now that it has been tested in the field for so many years. RTK cuts a lot of the original verbosity, but it also adds some more.&lt;/p&gt;

&lt;p&gt;Redux is awesome, because the &lt;strong&gt;&lt;a href="https://redux.js.org/understanding/thinking-in-redux/three-principles" rel="noopener noreferrer"&gt;Three Principles&lt;/a&gt;&lt;/strong&gt; (Single Source of Truth, Immutable State, and Pure Functions) allow us to build predictable, testable, and debuggable logic. But defining the state through reducers is not really intuitive; actions are best defined through action types and action creators; any impure logic has to be moved to thunks or other similar middlewares.&lt;/p&gt;

&lt;p&gt;Redux Toolkit (RTK) solved a few of these problems: slices are more intuitive, reducers can be written with mutable code, and actions are easier to define. But then we need to tell apart reducers and extraReducers, use the builder callback notation, and creating async thunks &lt;strong&gt;&lt;a href="https://redux-toolkit.js.org/api/createSlice#createasyncthunk" rel="noopener noreferrer"&gt;is not immediate&lt;/a&gt;&lt;/strong&gt;. It's an amazing piece of technology, it powers RTK Query, and I use it everyday at work, but it's not really the simplest.&lt;/p&gt;

&lt;p&gt;That's probably why so many other state management libraries started to emerge: Zustand, Jotai, Recoil, MobX, ... each one tries to simplify state management. And they do! But they had to sacrifice a few things: limited testability and predictability, not 100% compatible with time-travel debugging, ... Most of these libraries are perfect for small states, like in a Next.js server-side rendered page.&lt;/p&gt;

&lt;p&gt;What if we could have a new library that is 100% compatible with Redux, maintains the Three Principles (mostly) unvaried, but allows to do more with less code?&lt;/p&gt;

&lt;p&gt;Enter the Inglorious Store. It's basically a drop-in replacement for Redux, so it's 100% compatible with &lt;code&gt;react-redux&lt;/code&gt; and the Redux DevTools. The difference is mainly on how you write your code with it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can easily manage &lt;strong&gt;multiple instances&lt;/strong&gt;. Say you have a dashboard with some widgets: it's very intuitive to define a state that describes those widgets, and you can even easily add and remove them at runtime.&lt;/li&gt;
&lt;li&gt;It lessens the constraint of pure functions, allowing a &lt;strong&gt;reducer to dispatch an event&lt;/strong&gt;. Your code will still be predictable and testable though, because...&lt;/li&gt;
&lt;li&gt;Your events are sent to an &lt;strong&gt;event queue&lt;/strong&gt;, like a publisher/subscriber bus. If you use &lt;code&gt;batched&lt;/code&gt; mode, you can even process multiple events before updating your state (thus minimizing re-renders).&lt;/li&gt;
&lt;li&gt;The store uses an &lt;strong&gt;immutable library&lt;/strong&gt; like Immer (it's actually Mutative, who claims to be 10x faster) under the hood, so you can write your immutable code as if it was mutable.&lt;/li&gt;
&lt;li&gt;You don't need thunks or anything. Your event handlers &lt;strong&gt;can be asynchronous functions&lt;/strong&gt;. Just don't try to mutate the state right after a promise because it's not going to work. Just dispatch another event, like you're used to with &lt;code&gt;redux-thunk&lt;/code&gt; or &lt;code&gt;redux-saga&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You don't need to define action types and action creators. Just &lt;strong&gt;notify events&lt;/strong&gt; in a concise way.&lt;/li&gt;
&lt;li&gt;Are you scared that you will mistype an action name without action creators and action types? Just use TypeScript: the Inglorious Store is completely &lt;strong&gt;typesafe&lt;/strong&gt;, and it's really trivial to add types to your logic (no &lt;code&gt;const increment: CaseReducer&amp;lt;State, PayloadAction&amp;lt;number&amp;gt;&amp;gt; = (state, action) =&amp;gt; state + action.payload&lt;/code&gt;, just &lt;code&gt;const increment = (entity: CounterEntity, amount: number) =&amp;gt; entity.value + amount&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;It features &lt;strong&gt;built-in events&lt;/strong&gt; for entity creation and destruction, and even for hot-replacing reducers.&lt;/li&gt;
&lt;li&gt;It features &lt;strong&gt;lifecycle events&lt;/strong&gt; for when an entity has been created or destroyed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/@inglorious/store" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/strong&gt; is pretty exhaustive, so I'm not going to repeat myself here. Just know that it's open source, that I put my heart in it, and that I would really appreciate it if you could try it out and see if it suits your needs.&lt;/p&gt;

&lt;p&gt;You can find code examples here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/IngloriousCoderz/inglorious-engine/tree/main/examples/apps/todomvc" rel="noopener noreferrer"&gt;TodoMVC&lt;/a&gt;&lt;/strong&gt;: An (ugly) clone of Kent Dodds' &lt;strong&gt;&lt;a href="https://todomvc.com/" rel="noopener noreferrer"&gt;TodoMVC&lt;/a&gt;&lt;/strong&gt; experiments, showing the full compatibility with react-redux and The Redux DevTools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/IngloriousCoderz/inglorious-engine/tree/main/examples/apps/todomvc-cs" rel="noopener noreferrer"&gt;TodoMVC-CS&lt;/a&gt;&lt;/strong&gt; - A client-server version of the TodoMVC, which showcases the use of notify as a cleaner alternative to dispatch and async event handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/IngloriousCoderz/inglorious-engine/tree/main/examples/apps/todomvc-rt" rel="noopener noreferrer"&gt;TodoMVC-RT&lt;/a&gt;&lt;/strong&gt; - A "multiplayer" version, in which multiple clients are able to synchronize through a real-time server. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/IngloriousCoderz/inglorious-engine/tree/main/examples/apps/todomvc-ts" rel="noopener noreferrer"&gt;TodoMVC-TS&lt;/a&gt;&lt;/strong&gt; - A typesafe version of the base TodoMVC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>redux</category>
      <category>opensource</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
