Skip to main content

Declarative and imperative UI — deriving UI from state

React, Vue, and Svelte are all described as "declarative UI". Code written in jQuery, on the other hand, is called "imperative". The difference isn't a fashion in coding style — it's a design fork about how to prevent drift between state and rendering.

What you'll learn in this chapter​

  • What imperative UI and declarative UI are each writing
  • Why declarative UI reduces mismatches between state and rendering
  • Situations where you still need to touch the DOM directly under declarative UI
  • The price declarative UI is paying
Prerequisites

Having operated the DOM in JavaScript makes the comparison land. No framework experience is needed.

The same screen written two ways​

Consider a screen that shows "a spinner while loading, an error message on failure, and the list on success".

Written imperatively​

// Imperative — list out the steps that change the DOM
function showLoading() {
document.querySelector('#spinner').style.display = 'block';
document.querySelector('#error').style.display = 'none';
document.querySelector('#list').style.display = 'none';
}

function showError(message) {
document.querySelector('#spinner').style.display = 'none';
document.querySelector('#error').style.display = 'block';
document.querySelector('#error').textContent = message;
document.querySelector('#list').style.display = 'none';
}

function showList(items) {
document.querySelector('#spinner').style.display = 'none';
document.querySelector('#error').style.display = 'none';
const list = document.querySelector('#list');
list.style.display = 'block';
list.textContent = '';
items.forEach((i) => {
const li = document.createElement('li');
li.textContent = i.name;
list.appendChild(li);
});
}

What you're writing is the sequence of steps that change the DOM. Each of the three functions is responsible for the display of all three elements.

Written declaratively​

// Declarative — write the mapping from state to screen
type Status =
| {kind: 'loading'}
| {kind: 'error'; message: string}
| {kind: 'success'; items: Item[]};

function ItemList({status}: {status: Status}) {
if (status.kind === 'loading') return <Spinner />;
if (status.kind === 'error') return <ErrorText>{status.message}</ErrorText>;
return (
<ul>
{status.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

What you're writing is the mapping from state to screen. Not a single line describes how to change the DOM. When the state changes, the framework applies the update only where it is needed.

What each is describing​

Imperative UIDeclarative UI
What you writeSteps that change the DOMThe mapping from state to screen
DOM manipulationDeveloper writes the stepsFramework derives the diff
Typical examplesjQuery, raw DOM APIReact, Vue, Svelte
Where state livesIn the DOM itself, or in both variables and the DOMIn state variables (a single source of truth)

Under imperative UI, "how the display currently looks" is scattered across the DOM. Under declarative UI, only the state variables are the truth, and the screen is derived from them every time.

Why does the mismatch shrink?​

Imperative UI is fragile because the display of every element has to be spelled out again in each of the per-state functions.

With three states and three elements to look after, that's 3 × 3 = 9 display instructions. Four states and four elements make 16. Every state has to say, for every element, whether it is hidden or shown.

Look at showError from earlier. It hides the spinner, shows the error, and hides the list. It touches all three elements. Adding a fourth element (say, a "Retry" button) means all three existing functions need edits. Miss one, and you end up with an error screen where the retry button stays hidden.

Under declarative UI, you write as many branches as there are states, and each branch lists only the elements that state should show. Adding the retry button means adding one line in the branches where it should appear. The other branches are already returning "the finished picture for that state", so they stay correct without you thinking about them.

This is why people say declarative UI has fewer state/UI mismatches. The reduction in mismatches is an advantage of declarative UI, not a disadvantage.

You still touch the DOM directly in declarative UI​

"Declarative UI means no DOM operations at all" is not true. Operations that can't be derived from state remain.

  • Focusing an input
  • Scrolling to a specific position
  • Playing or pausing video and audio
  • Drawing on a canvas
  • Measuring the actual size of an element

These are "what to do at this exact moment", not "how the screen looks now", so they can't be expressed as a mapping from state. React provides a way out for these operations called ref.

So declarative UI is not a mechanism that bans DOM operations. It replaces most of them with derivation from state, and isolates the rest as explicit exceptions.

The price​

Declarative UI isn't free. The table below has React and Vue in mind, which take a diff over a virtual DOM; with an approach like Svelte's, which generates update code at compile time, the weight of the first two rows changes.

CostWhat
Runtime weightYou have to ship the diffing machinery to the browser
Cost of diffingEvery state change triggers a comparison against the previous result
Indirection of control"Why did the DOM end up this way?" is hidden inside the framework
Learning costYou need to learn state design, re-render conditions, and side-effect handling

For a small landing page with one animated element, plain DOM APIs can be lighter and faster. Declarative UI pays off in applications with many kinds of state, where each state affects multiple parts of the screen. The React Guide — State covers how to design state in practice.

Common misconceptions​

"Declarative UI is more prone to state/UI mismatches." — The opposite. Reducing mismatches is declarative UI's main motivation. Because the screen is derived from state every time, the failure mode of forgetting to update one of the per-state functions simply disappears.

"In declarative UI, the developer controls the DOM operations in fine detail." — That describes imperative UI. Declarative UI doesn't write steps; it writes the mapping between state and rendering.

"Declarative UI never touches the DOM." — Focus, scroll, media playback, and similar operations can't be derived from state, so they are done directly through an explicit path like a ref.

"Writing declaratively makes it faster." — Not necessarily. There's runtime cost for diffing. What you gain is correctness and maintainability as state grows, not raw execution speed.

Exercises​

Q1. Is the code below declarative or imperative? Explain why.

const badge = document.querySelector('#cart-badge');
badge.textContent = String(count);
badge.classList.toggle('hidden', count === 0);
Answer and explanation

Answer: Imperative

It writes the steps of an operation: fetch the element, rewrite the text, toggle a class. The relationship "hide when count is zero, otherwise show the count" isn't apparent from the shape of the code — you have to trace three lines and assemble it.

Written declaratively, the same thing looks like this. The mapping appears directly as an expression.

function CartBadge({count}: {count: number}) {
if (count === 0) return null;
return <span className="badge">{count}</span>;
}

Q2. Under imperative UI, if states grow to four and the elements to look after grow to four, how many display instructions must be written?

Answer and explanation

Answer: 16 (4 × 4)

Each per-state function says, for each of the four elements, whether it is hidden or shown: 4 × 4 = 16. Add one more element and it becomes 4 × 5 = 20, with edits needed in all four functions.

In declarative UI you write as many branches as there are states — four branches for four states. Each branch lists only the elements that state should show, so adding an element means adding a line only in the branches that show it. That gap becomes a gap in maintainability.

Q3. In a project using declarative UI, you want "when the submit button is pressed, return focus to the input". Should focus be held as state?

Answer and explanation

Answer: Not as state — use a ref to operate directly.

Focus is "an operation to perform once, at a specific moment", not something expressible as "for this state, it looks like this". Even if you introduced a shouldFocus state, you'd need to reset it after focusing, which just adds imperative steps.

Declarative UI doesn't ban DOM operations. It divides labor: derive what can be derived from state, and pull out what can't as an explicit exception via a ref.

Summary​

  • Imperative UI writes the steps that change the current screen; declarative UI writes the result of "for this state, it looks like this"
  • React and Vue are examples of declarative UI; jQuery is an example of imperative UI
  • Under imperative UI, every per-state function repeats the display of every element. Under declarative UI, you write as many branches as there are states, each listing only the elements that state shows
  • The main motivation for declarative UI is reducing state/UI mismatches. It doesn't increase them
  • Operations that can't be derived from state — focus, scroll, media playback — still touch the DOM directly in declarative UI
  • The price is runtime weight, the cost of diffing, and indirection of control
Related references