DEV Community

Cover image for You Just Need Entities That Can Die
Matteo Antony Mistretta
Matteo Antony Mistretta

Posted on

You Just Need Entities That Can Die

This post is part of a series of corollaries to the Inglorious Web series. It stands alone, but the examples make more sense if you've read the architecture post first.


Forms are one of those relevant cases where the Single Source of Truth principle goes to die.

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.

Redux-form was that mistake, made by Erik Rasmussen — 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 React Final Form instead. The lesson it taught the ecosystem shaped every form library that followed.

The real axis here isn't global vs local. It's alive vs dead. 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.

In Redux-form, you reset forms. In Formik, you isolate them. In Inglorious Web, you destroy them.


Redux-Form: When the Principle Became the Problem

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.

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.

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.


Formik and React Final Form: Escaping the Store

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 useRef and useState internally, keeping Redux out of it entirely. Clean, practical, and widely adopted.

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 FormSpy: a component that could subscribe to specific slices of form state and re-render independently, minimizing the rendering surface area.

<FormSpy subscription={{ values: true }}>
  {({ values }) => (
    <pre>{JSON.stringify(values, null, 2)}</pre>
  )}
</FormSpy>
Enter fullscreen mode Exit fullscreen mode

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

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.


Inglorious Web: Forms as Entities

In Inglorious Web, a form is an entity. You compose the built-in form primitive with your own type, add a submit handler, and declare the entity in the store with its initial values:

import { Form } from '@inglorious/web/form'
import { html } from '@inglorious/web'

const ContactForm = {
  ...Form,

  create(entity) {
    entity.values = { name: '', email: '' }
  },

  submit(entity, _, api) {
    api.notify('contactFormSubmit', entity.values)
    api.notify('remove', entity.id) // optional — destroy the entity on submit
  },

  render(entity, api) {
    return html`
      <form @submit=${() => api.notify(`#${entity.id}:submit`)}>
        <input
          .value=${entity.values.name}
          @input=${(e) => api.notify(`#${entity.id}:fieldChange`, {
            path: 'name',
            value: e.target.value,
          })}
        />
        <input
          .value=${entity.values.email}
          @input=${(e) => api.notify(`#${entity.id}:fieldChange`, {
            path: 'email',
            value: e.target.value,
          })}
        />
        <button type="submit">Send</button>
      </form>
    `
  },
}

const store = createStore({
  types: { ContactForm },
  autoCreateEntities: true
})
Enter fullscreen mode Exit fullscreen mode

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 submit 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 remove:

api.notify('remove', entity.id)
Enter fullscreen mode Exit fullscreen mode

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

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 Redux DevTools, time-travelable, and accessible to any other entity via api.getEntity(). 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.

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

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.

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 render method shows the relevant fields based on a currentStep property, and the entity is destroyed only when the whole flow completes. Either way, the store stays flat and normalized.


The FormSpy Problem Is Less Acute

React Final Form's FormSpy 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.

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 FormSpy equivalent to learn.

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 benchmarks post has the full picture.


The Pattern Again

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.

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.

You just need entities that can die.


Docs · Repo

Top comments (0)