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
stateand callrender(). One source of truth, no drift. stateis 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.