Frontend

The render loop is a state machine in disguise

Rethinking component updates as transitions instead of mutations made a tangled UI suddenly tractable.

JI
Jungin
2026 · 04 · 21 · 1 min read
figure — states, not mutations

For a long time I thought of UI as a pile of mutations: this click sets that flag, which toggles this class, which maybe resets that field. It worked until it didn’t, and the day it didn’t there was no single place to look.

Transitions, not mutations

The shift was small to write and large to think about: stop describing what changes and start describing what state we are in. A component is in one of a finite set of states, and events move it between them.

type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; message: string }
  | { status: "ready"; items: Item[] };

function next(state: State, event: Event): State {
  if (state.status === "loading" && event.type === "resolved") {
    return { status: "ready", items: event.items };
  }
  // ...one arm per legal transition
  return state;
}

Suddenly the impossible states are unrepresentable — there is no way to be loading and error at once, because the type won’t let you.

The payoff

Once updates are transitions, the render function gets boring in the best way: it is a pure projection of the current state onto markup. Debugging becomes “which state are we in, and was this transition legal?” — two questions with definite answers, instead of an archaeology dig through a dozen mutations.

jungin.dev

A personal journal by Jungin on software, with frequent detours into math and physics.