I'm Done With Magic. Here's What I Built Instead.

Published: (February 17, 2026 at 08:30 AM EST)
8 min read
Source: Dev.to

Source: Dev.to

The JavaScript ecosystem has a magic problem.
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 computed() stopped updating, or why an effect fired when you didn’t expect it, or why destructuring a store value makes it stop being reactive.

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.

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.

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.


Redux’s three principles

I really started digging React when they introduced Redux. It revived 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:

  1. Single Source Of Truth – one state to rule them all.
  2. State Is Read‑Only – reference comparisons make re‑render decisions trivial and performant.
  3. Changes Through Pure Functions – reducers make logic trivial to reason about.

But things went south. Developers 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. Then it kept going — createAppSlice, 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.

Soon 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.


Why “magic” is a problem

Let me be specific, because “magic is bad” is an easy claim to make and a hard one to defend without evidence.

  • React – Re‑renders are actually fast; React was right about that. The real problem is that re‑renders trigger effects and lifecycle methods. useEffect fires after every matching render, subscriptions re‑initialize, derived state recomputes. Invisible dependency arrays silently break when you forget something, and useEffect lists grow into things nobody on the team fully trusts. React’s answer? A stable compiler that adds layers of cache automatically. That means you can have a sub‑optimal component hierarchy and the compiler will compensate — convenient until you need to understand why something broke.

  • Vue 3 – The Composition API introduces a subtle trap: destructuring a reactive object silently breaks the proxy chain that powers reactivity. Your variable stops updating and you get no warning whatsoever. Vue provides toRefs() 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 computed() 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.

  • Svelte 5 – Introduced runes ($state(), $derived(), $effect()). The docs define the word:

    rune /ruːn/ noun — A letter or mark used as a mystical or magic symbol.

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

All three are racing in the same direction: more reactivity, more compilation, more invisible machinery. I went the other way.


Inglorious Web

Inglorious Web is built on one idea: state is data, behavior are functions, rendering is a pure function of state.

  • 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.

const counter = {
  create(entity) {
    entity.value = 0;
  },

  increment(entity) {
    entity.value++;
  },

  render(entity, api) {
    return html`
      
        Count: ${entity.value}
         api.notify(`#${entity.id}:increment`)}>
          +1
        
      
    `;
  },
};

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 absent:

  • No hooks.
  • No lifecycle methods.
  • No component‑level state.

create and increment 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.

A deliberate abstraction

One abstraction worth naming: state mutations inside handlers look impure but aren’t. The framework wraps them in Mutative — the same structural‑sharing idea as Immer, but 2–6× faster — so you write mutable‑style code while the library produces immutable updates under the hood.

Reactive Simplicity

entity.value++ returns an immutable snapshot. That’s the only reactive magic in the stack—a small, well‑understood library that makes testing trivial.

When state changes, the whole tree re‑renders, but lit‑html only touches the DOM nodes that actually changed—just as Redux reducers do nothing 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 exactly when it runs. Since 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.

Comparison with Other Frameworks

  • React – testing a component with hooks means setting up a fake component tree and mocking the world around it.
  • Vue 3 – testing a composable means testing impure functions swimming in proxy magic.
  • Inglorious Web – testing state logic is straightforward:
import { trigger } from "@inglorious/web/test";

const { entity, events } = trigger(
  { type: "counter", id: "counter1", value: 10 },
  counter.increment,
  5,
);

expect(entity.value).toBe(15);

Testing rendering is equally straightforward:

import { render } from "@inglorious/web/test";

const template = counter.render(
  { id: "counter1", type: "counter", value: 42 },
  { notify: vi.fn() },
);

const root = document.createElement("div");
render(template, root);

expect(root.textContent).toContain("Count: 42");
// snapshot testing works too:
expect(root.innerHTML).toMatchSnapshot();

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.

Architecture Overview

  • React, Vue, Svelte – component‑centric. The component is the unit; logic lives in components, state is owned or lifted by them, everything forms a tree.
  • Inglorious Web – 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.

If you’ve heard of the Entity Component System (ECS) architecture used in game engines, this will feel familiar—though it isn’t a strict implementation. Think of it as ECS meets Redux:

  • Entities hold data.
  • Types hold behavior.
  • 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.


Series Overview

This is the first post in a series.

Next Post

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 trade‑offs, and where the framework fits (and where it doesn’t).

Third Post

I’ll show the numbers: a benchmark running 1 000 rows at 100 updates per second, comparing React (naive, memoized, and with RTK) and a live‑chart benchmark against Recharts. We’ll look at performance, bundle size, and what “dramatically smaller optimization surface area” actually looks like in practice.


The ecosystem is moving toward more magic. I’m moving the other way.

Docs · Repo

0 views
Back to Blog

Related posts

Read more »