Skip to main content

Stateful UI Components

intermediate15 min readLesson 66 of 143

Structure a widget as state + render + events: a component factory pattern that scales from a modal to a full dashboard.

Beginner scripts grow into tangles: every feature reaches into the DOM from everywhere. The intermediate move is to give each widget a component shape: private state, a render function that draws state to the DOM, and events that only update state.

The factory pattern

function createTabs(root) {
  const state = { active: 0 }; // 1. private state (a closure)
  const buttons = [...root.querySelectorAll("[role=tab]")];

  function render() {
    // 2. state โ†’ DOM
    buttons.forEach((b, i) => {
      b.setAttribute("aria-selected", String(i === state.active));
    });
  }

  root.addEventListener("click", (e) => {
    // 3. events โ†’ state
    const b = e.target.closest("[role=tab]");
    if (!b) return;
    state.active = buttons.indexOf(b);
    render();
  });

  render(); // initial draw
}

The rules that keep it clean:

  • Events never edit the DOM directly โ€” they update state and call render(). One source of truth, no drift.
  • state is closed over: nothing outside can poke it. Changes flow through the component's own API.
  • render() is idempotent โ€” call it ten times, get the same UI.

Accessibility is part of the pattern

Components carry semantics: role, aria-selected, aria-expanded, focus moved into modals and back. Doing accessibility at the component layer means every instance inherits it โ€” retrofitting it later is the expensive path.

Now practice

Stateful Components โ€” PracticeComponent factories without a framework: a reducer-driven tabs widget, a modal with focus return, and an accordion.2 challenges ยท ยท ~20 min