Frontend
Lesson 3 of 8About 3 min readSuggest an edit

State and rendering in UI frameworks

React, Vue, Svelte, Solid and Preact differ in syntax, but they share one idea: UI is a function of state. You describe what the screen should look like for the current data, and the framework works out which DOM changes are needed.

Props and state

  • Props are inputs a parent passes to a child. The child treats them as read-only.
  • State is data a component owns and can change: whether a menu is open, the text in a search box.

When state changes, the component renders again with the new values, and so do the children that receive the changed data as props.

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>;
}

Calling setCount doesn’t change count immediately. It schedules a new render, in which count holds the new value.

Why you must not mutate state

Frameworks detect changes in different ways. React compares references: if you mutate an array in place and pass the same array back, React sees “same object” and may skip the update.

// Bug: same array object, React may not re-render
items.push(newItem);
setItems(items);

// Correct: a new array
setItems([...items, newItem]);

Vue and Svelte track mutations through proxies or compiler magic, so in-place changes can work there, which is exactly why code moved between frameworks breaks.

Derived state: compute, don’t store

If a value can be calculated from existing state or props, calculate it during render instead of storing a copy.

// Two sources of truth that can drift apart
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);

// One source of truth
const [items, setItems] = useState([]);
const count = items.length;

Duplicated state is one of the most common sources of UI bugs: one copy gets updated and the other doesn’t.

Where state should live

Put state in the lowest common parent of every component that needs it. If two siblings both need the selected tab, the state lives in their parent and flows down as props (“lifting state up”). Reach for a global store only for data that really is global, such as the signed-in user.

Effects are for syncing with the outside world

Effects (useEffect, watch, onMount) run after rendering. They exist to keep something outside the component in sync: a subscription, a timer, a network request, a DOM API.

useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id); // cleanup when the component goes away
}, []);

Two common mistakes:

  • Using an effect to compute derived data. It causes an extra render and a moment of stale UI. Compute it during render instead.
  • Stale closures. A callback created during one render keeps seeing that render’s values. If an event listener registered once reads state, read it through a ref or re-register the listener when the state changes.

Keys in lists

When rendering a list, give each item a stable, unique key, usually an id from your data. Keys let the framework match old and new items, so it can move DOM nodes instead of recreating them. Using the array index as the key breaks when items are inserted or reordered: input values and focus stick to the wrong rows.

Performance, last

Rendering is usually cheap. When it isn’t, measure first with the framework’s profiler, then reach for memoization (memo, useMemo, computed) or virtualized lists for long collections. Premature memoization adds complexity without making anything faster.

Next: CSS layout with flexbox and grid

When to use flexbox or grid, how sizing works, and how to fix common layout bugs.