Choose one owner for each piece of information

A component becomes difficult to reason about when the same fact is stored in several places. Imagine a quantity selector with a JavaScript count, a number written in a DOM node, and a separate Boolean that says whether the decrement button should be disabled. If each value can change independently, every event handler must keep all three in agreement. A missed update leaves the interface showing one thing and doing another.

Instead, make the count the component’s source of truth. The displayed number and disabled status are consequences of that count: the button is disabled when the count is zero. This does not mean copying the entire DOM into a state object. Store values that users or external events can change, then derive presentation details when rendering. A useful design question is: if this value disappeared, could I calculate it reliably from something else I already have? If so, it probably does not need its own state field.

Make state changes flow through a render function

The following example runs in a modern browser with JavaScript enabled. Open a blank page, paste the code into its developer console, and press Enter. It uses only built-in DOM APIs and creates its own elements; no framework, package, or existing page markup is required.

(() => { let count = 0; const container = document.createElement('div'); const decrease = document.createElement('button'); const value = document.createElement('span'); const increase = document.createElement('button'); decrease.type = 'button'; increase.type = 'button'; decrease.textContent = 'Decrease'; increase.textContent = 'Increase'; container.append(decrease, value, increase); document.body.append(container); function render() { value.textContent = ` Quantity: ${count} `; decrease.disabled = count === 0; } decrease.addEventListener('click', () => { count = Math.max(0, count - 1); render(); }); increase.addEventListener('click', () => { count += 1; render(); }); render(); })();

Both handlers change only count, then call render. Neither reads the displayed text to determine the next count, and neither maintains a separate disabled flag. Render is safe to call again with the same count because it assigns the same observable values. In a larger component, a single update function can centralize state changes and rendering, but the underlying rule stays the same: decide what changed, update its owner, and reflect the result in the DOM.

Treat form controls as a deliberate boundary

Inputs need extra care because the browser updates their values while a person types. One valid approach is to let an input own its in-progress text and read it when the user submits. Another is to keep the value in component state, listen for input events, and set the DOM value when rendering. Choose one approach for a given value rather than occasionally reading the input and occasionally trusting an unrelated state copy.

If a form needs immediate validation, the input event can update a draft string in state and trigger rendering. Keep that draft distinct from a confirmed value if confirmation is a real step in the workflow; they represent different facts, not duplicate copies of one fact. Avoid rewriting a focused input unnecessarily, since doing so can disturb the caret or selection. The same principle applies to checkboxes and selects: decide whether the browser or the component owns the current value, then make the handoff explicit.

Handle changes that arrive outside the component

Duplicate truth can also enter through shared data. If a parent supplies a selected item, a child should not automatically copy it into private state and then expect both copies to remain synchronized. The child can render directly from the supplied value and ask the parent to change it through a callback. Private state is appropriate for something genuinely local, such as whether that child’s menu is open.

Asynchronous results present a different risk: an older request may finish after a newer one. If the component displays search results, record which query or request a result belongs to before rendering it. Discard results that no longer match the current query. The DOM should show the accepted result of the current state, not whichever response happened to arrive last. Where possible, keep unrelated scripts from editing DOM nodes the component owns; if outside code must change them, define an explicit way to pass those changes back.

Test the relationship, not just the appearance

A useful component test performs an action and checks the resulting behavior as well as the text. For the quantity example, verify that the initial label says zero and Decrease is disabled. Click Increase, then check that the label says one and Decrease is enabled. Click Decrease and check that both return to their initial condition. These checks exercise the relationship between one stored count and two derived DOM properties.

Also test boundaries that often expose competing sources of truth: repeated clicks at a minimum value, editing an input before submission, receiving new data from a parent, and completing asynchronous work out of order. If a test has to alter both component state and DOM text to prepare one scenario, that is a sign the component may have two owners for the same fact. Aim for a design where each event has one clear state transition and rendering makes the result visible.

Back to category ↗