State → Render: UIs Without a Framework
Advanced · 20 min read · ▶ live playground · ✦ checkpoint
Storage decides what can survive later. State decides what the page should show right now. Once a UI has more than one moving part, the cleanest pattern is to keep one JavaScript object as the truth, then render the page from that object every time something changes.
This is the missing step between "I can change the DOM" and "I can build an interface." You stop chasing individual elements around the page and start asking one question: what should the screen look like for this state?
One object owns the truth
A state object is a plain object that holds the current facts your UI cares about. For a task board, that might mean the tasks, the active filter, and the next ID to use:
const state = {
filter: "all",
nextId: 4,
tasks: [
{ id: 1, title: "Draft launch note", done: false },
{ id: 2, title: "Review invoices", done: true },
{ id: 3, title: "Book studio room", done: false },
],
};
console.log(state.tasks.length);
console.log(state.tasks.filter((task) => !task.done).length);The state object is the single source of truth: one place your code trusts for the current answer. The DOM becomes a display of that answer, not a second copy you manually keep in sync.
That distinction matters. If one handler updates a checkbox, another updates a counter, and a third removes an empty message, your UI can drift into disagreement. The checkbox says two tasks are done, the counter says one, and the empty message still appears. A single source of truth prevents that by making the DOM disposable: when state changes, rebuild the visible pieces from state.
Render is a pure-ish translation
Rendering means turning state into DOM. A render(state) function reads the current state and paints the matching page: text, classes, lists, disabled buttons, empty messages, and counts.
The render function should not decide what happened. It should answer "given this state, what should be visible?" That keeps event code and display code separate:
event happens
update state
render(state)Here is the important rule: event handlers update data first, then call render. They do not scatter one-off DOM edits as the main pattern.
Try it - a state-driven task board
Run the playground, then try these checks: add a task, mark it done, switch filters, and delete a task. Watch the list, counts, empty message, and selected filter stay in agreement because they all come from the same state object.
Two details are doing a lot of work.
First, the handler never updates the count directly. It updates state.tasks, then render(state) calculates the count from state. That is a derived value, a value you compute from the source truth instead of storing separately.
Second, deleting a task does not need custom code to update every possible view. If the current filter is "done," deletion still works because render re-asks visibleTasks(state) after the state change.
Contrast: scattered DOM edits drift
Direct DOM edits are not bad. You have used them all through Part IV. They become fragile when several pieces of UI describe the same fact.
Imagine a "mark done" handler that checks the box, adds a class, changes the summary, hides the empty message, updates a debug panel, and remembers to do all of that again for every future feature. That code has many places to forget one tiny update.
The state-render version has a smaller contract:
- Event handler: "what fact changed?"
- State object: "what is true now?"
- Render function: "how should that truth look?"
That is why the playground can rebuild taskList.textContent = "" and recreate rows. For a small UI, rebuilding the list is clear and reliable. Bigger frameworks optimize the repainting step, but the mental model is the same.
Where state ends
State is memory by default. In this lesson, Run rebuilds the page and the state starts over. That is correct: 17.1 already separated persistence from state. You can later save part of state to storage, send it to a server, or encode it in the URL, but those are separate decisions.
Do not save every tiny UI fact. A counter like "open tasks" can be derived. A CSS class like is-selected can be rendered. A visible list can be rebuilt. The source truth is the smallest object that lets the page answer its real questions.
The framework-shaped loop
You just wrote the core loop behind modern UI work:
- Start with state.
- Render the page from state.
- Listen for events.
- Update state.
- Render again.
Frameworks add components, scheduling, efficient updates, dev tools, and conventions for large teams. They do not remove the need to think clearly about state. If anything, they reward it: a clean state shape makes framework code feel obvious later.
Next, you will move one kind of state out of memory and into the address bar. Routing and the History API let the URL describe what the user is looking at, so a view can be shared, bookmarked, and restored.
Checkpoint
Answer all three to mark this lesson complete