088: Why React?
Learning objective
Outcomes
React is a way to organize UI code, not a source of magic. By the end of this material, you should be able to compare imperative DOM updates with declarative rendering, choose useful component boundaries, define state, and explain a re-render without saying that React reloads the page.
You should be able to explain the problem React addresses and sketch a component tree for a task manager.
Prerequisites
You should be comfortable with the completed JavaScript module (042–087), including functions, arrays, objects, modules, DOM events, forms, Promises, fetch, and HTTP/API basics. If selecting DOM elements, submitting forms, or handling asynchronous requests still feels unfamiliar, review those JavaScript topics before continuing.
Retrieval practice
Before reading, try to answer these from memory:
- How would vanilla JavaScript create an
li, set its text, and append it? - What browser event fires when a user submits a form?
- Why is data usually easier to update in an array than when scattered through DOM nodes?
Content to cover
imperative DOM vs declarative UI; component model; state; re-render concept.
Terms and mental model
The problem with imperative UI code is not that any individual DOM operation is difficult. The problem is that the code must keep track of every operation needed to keep the screen consistent. Imperative code says how to perform each operation: find this element, remove that class, and change this text. Declarative code describes what the UI should look like for the current data. React calls component functions to calculate JSX, then commits the necessary DOM changes.
- Component: A JavaScript function returning a UI description; its name begins with a capital letter. — Source: React: Your first component
- Props: Read-only inputs a parent passes down to customize its child component. — Source: React: Passing props
- State: A component’s memory for data that changes over time due to interaction or external systems. — Source: React: useState
- Render: React calling your components to calculate the UI snapshot for current props and state. — Source: React: Render and commit
- Commit: React applying the minimal necessary changes to the browser DOM. — Source: React: Render and commit
- Re-render: Another render pass triggered by changed state, props, or context — not a page reload. — Source: React: Render and commit
The spreadsheet analogy is useful here. When you edit an input cell, formulas recalculate and the displayed cells that depend on it update. You describe the relationships instead of manually repainting every dependent cell. React similarly derives UI from data. The analogy has a limit: unlike a spreadsheet, component functions must remain pure while rendering. Given the same inputs, they should return the same JSX and should not mutate the DOM or make a network request in their component body.
Imperative versus declarative
Here is a small imperative vanilla-JavaScript task counter:
const list = document.querySelector('#tasks');
const count = document.querySelector('#count');
function addTask(title) {
const item = document.createElement('li');
item.textContent = title;
list.append(item);
count.textContent = `${list.children.length} tasks`;
}
The code works, but every operation has to remember every part of the DOM it affects. Once filtering, deletion, editing, and loading states are added, the number of synchronization paths grows quickly. React does not remove those product requirements. It gives you a clearer place to express the relationship between the data and the visible output:
function TaskSummary({ tasks }) {
return <p>{tasks.length} tasks</p>;
}
When tasks changes, the next render calculates the correct count. React is not making the application complexity disappear; it is keeping the relationship between data and UI in one declarative calculation. That still depends on having sound data and update rules.
Beginner complete example
For now, use the React playground at react.dev, or put this in a Vite src/App.jsx later. It is intentionally a complete, single-file component example with no interaction yet.
const tasks = [
{ id: 't1', title: 'Read the React mental model', completed: true },
{ id: 't2', title: 'Sketch component boundaries', completed: false },
];
function TaskItem({ task }) {
return (
<li>
<span>{task.completed ? 'Complete: ' : 'Open: '}</span>
{task.title}
</li>
);
}
function TaskList({ tasks }) {
return (
<ul>
{tasks.map((task) => (
<TaskItem key={task.id} task={task} />
))}
</ul>
);
}
export default function App() {
return (
<main>
<h1>Task Manager</h1>
<p>{tasks.filter((task) => !task.completed).length} open tasks</p>
<TaskList tasks={tasks} />
</main>
);
}
The data is separate from the markup. App composes TaskList, and TaskList creates one TaskItem for each task. The open-task count is derived from the same array that supplies the list, so the array is the single source of truth. There is no separately maintained count that can become stale.
Intermediate: identify boundaries
Do not start with a rule that every div deserves its own component. Start with what the user and developer need the UI to do. For this task manager, a practical first tree is:
App
├─ Header
├─ TaskForm
├─ TaskFilters
├─ TaskList
│ └─ TaskItem (repeated)
└─ TaskSummary
Extract a component when it has a recognizable responsibility, repeats, becomes complex, or would benefit from independent testing. Keep closely related markup together. A component named TaskTitleTextWrapper would add a file without adding a useful concept, so it would make the intent harder to see rather than clearer.
Where should changing task data live? Put state in the closest common parent of every component that reads or changes it. If the form adds tasks and the list displays them, App is a likely owner. App can pass the data and event callbacks down. A child should not reach sideways into a sibling; shared data should travel through their common parent.
Optional advanced: what React actually updates
Calling a state setter queues a render. React calculates a new render tree and compares it with the previous tree using identity, element type, keys, and props. During commit, it performs the DOM operations that are necessary. A parent render may call a child component again even when that child’s DOM output does not change. In other words, “re-render” means recalculation, not “rewrite the whole DOM.”
Do not add useMemo or useCallback merely because a component renders. First measure an actual performance problem. Current React tooling, including the React Compiler where it is configured, further reduces the need for speculative manual memoization.
Mistakes and debugging
- Treating React as a templating language only: React’s useful model also includes components, state, events, and predictable data flow.
- Mutating DOM managed by React: direct
querySelector(...).textContent = ...can conflict with the next commit. Describe the change through state instead. - Performing side effects while rendering: network requests, timers, and DOM writes make render impure.
- Duplicating facts: storing both
tasksandopenCountcreates synchronization bugs. CalculateopenCountfromtasks. - Extracting every tag: too many tiny files make the component tree harder to follow.
- Assuming a render is slow: use browser and React profiling before optimizing.
When debugging, ask a concrete sequence of questions: What data produced this screen? Which component owns that data? Was the source mutated? Is the displayed value derivable? React Developer Tools can show component props and state; browser DevTools shows the resulting DOM. Those tools answer different parts of the problem.
Accessibility and performance
React does not turn inaccessible markup into accessible markup. Prefer semantic <main>, headings in order, real <button> controls, <form>, labels, and list elements. Do not make a clickable div. Later material covers the suitable text and live-region behavior for state changes that communicate loading or errors.
Component boundaries can help performance by localizing state, but correctness comes first. Keep state minimal and close to where it is needed. Avoid Effects that merely copy data and avoid manual memoization without evidence. The browser still has to download JavaScript before this client UI can run, so React is not automatically faster than a small static page.
Practice
Identify component boundaries in an existing UI.
Tiered exercises
Core: For a task manager containing a title, add form, filters, repeated tasks, and summary, draw a component tree. Mark repeated components and the likely state owner.
Stretch: Add an account menu and a reusable confirmation dialog. Decide whether each belongs inside TaskList, App, or a sibling branch and justify the decision.
Challenge: Write a short declarative UI table for { status: 'loading' | 'error' | 'ready', tasks: [] }: specify what should appear for each combination without listing DOM mutation steps.
Core solution
App (owns tasks and activeFilter)
├─ Header
├─ TaskForm (receives onAdd)
├─ TaskFilters (receives activeFilter and onFilterChange)
├─ TaskList (receives visibleTasks)
│ └─ TaskItem × n (receives task and event callbacks)
└─ TaskSummary (receives tasks or derived counts)
App is the closest common parent for the form, filters, list, and summary, so it is a sensible owner for the shared task state and active filter. TaskItem repeats and has a coherent responsibility.
Stretch solution
Put AccountMenu under Header if it concerns global navigation. Put ConfirmDialog near the state that decides whether deletion is pending, often App, and pass its content and callbacks as props. Do not nest a dialog in every task unless each item independently owns its dialog state.
Challenge solution
| Status | Tasks | UI |
|---|---|---|
| loading | unknown/old | heading plus “Loading tasks…” status |
| error | irrelevant | heading, error message, retry button |
| ready | empty | heading, form, filters, “No tasks yet” |
| ready | non-empty | heading, form, filters, task list, summary |
This table describes the output derived from state. Event handlers change the state; rendering selects the row that matches the current combination.
Exit questions
- What problem does this concept solve?
- What is one common mistake?
- Can you explain the code without reading it line by line?
Recap
React lets components declaratively calculate UI from props and state. A state change requests a render, and React then commits only the DOM changes that are needed. Components are useful responsibility boundaries, not wrappers for every element. Keep rendering pure, keep data minimal, and derive values such as counts instead of synchronizing duplicate state.
Official references
- React: Describing the UI
- React: Render and Commit
- React: Thinking in React
- React: Keeping Components Pure
React render, commit, identity, and reconciliation
A normal React update has a render phase, in which components calculate the next element tree, and a commit phase, in which React applies the necessary host-tree changes and runs relevant effects. A component function running again does not mean the browser DOM was replaced wholesale.
Identity is determined by position, element type, and keys. A stable key lets React associate a previous child with the same conceptual child after insertion, deletion, or sorting. Use React DevTools and a stateful child to observe that preservation instead of relying on memorized slogans.
Interview case: reconciliation and keys
Consider a focused input inside a list. If the first record is deleted, React should keep the input value with the same task.id, not with whichever record happens to occupy the same array position:
function Row({ task }) {
const [draft, setDraft] = useState(task.title);
return <input aria-label={task.title} value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
tasks.map((task) => <Row key={task.id} task={task} />);
Reconciliation compares the next element tree with the previous one. The same type and a stable key generally preserve the component instance; changing the key deliberately resets it. Keys do not make rendering faster by themselves, and React does not pass them as props.key. An index key fails when insertion, deletion, filtering, or reordering changes which entity occupies a position.
Interview answer: “React renders a new description, matches siblings by type and key, then commits the smallest host-tree change. A key is identity, so I use a durable domain ID. I would demonstrate correctness with a stateful row and an insertion test.”
Render causes and referential equality
A component can render because its state setter schedules an update, its parent renders it again, a consumed context value changes, or an external-store subscription updates. Render is calculation; commit applies host-tree changes. A parent render may call a child even when that child produces identical DOM.
Referential equality uses Object.is: objects and functions that look equal are still different when they are different references. That distinction can defeat shallow comparisons and retrigger Effects. This example creates a fresh options object on every render:
function Parent({ user }) {
const [tick, setTick] = useState(0);
const options = { userId: user.id }; // new reference every render
return <Child options={options} onPing={() => setTick((n) => n + 1)} />;
}
Before memoizing, keep state local and pass primitives where practical. If profiling identifies an expensive pure child, memo can skip equal props, useMemo can preserve an expensive derived value, and useCallback can preserve a function reference. None of these makes impure rendering safe or prevents updates caused by changed context, and each introduces comparison or dependency-management cost.
Runnable probe and tests
import { memo, useState } from 'react';
const Child = memo(function Child({ label }) {
console.log('Child render');
return <p>{label}</p>;
});
export default function RenderProbe() {
const [count, setCount] = useState(0);
return <main><button onClick={() => setCount((n) => n + 1)}>Increment</button><button onClick={() => setCount((n) => n)}>Same value</button><Child label="stable primitive" /><p>Count: {count}</p></main>;
}
Test the user-visible contract: click Increment and expect Count: 1; click Same value and expect Count: 0. Use the React DevTools Profiler for evidence about rendering rather than relying on brittle render-count assertions, because Strict Mode and scheduling affect development calls. Useful interview follow-ups are: what schedules work without changing the DOM, why can a parent call a child again, and when is keeping state local better than using memo?
2026 depth expansion: React's actual execution contract
With React 19.2, the most useful model is more precise than “React updates the DOM.” A typical path looks like this:
event / external change
↓
schedule an update
↓
render phase
calculate the next UI tree
(must stay pure and restartable)
↓
commit phase
apply the chosen changes
↓
browser layout + paint
↓
Effects synchronize external systems
A render can begin and later be abandoned. That is why mutating a module variable, writing to storage, starting a request, or changing the DOM during render is a correctness bug, not merely a matter of style preference.
Purity is an architectural requirement
A component should act like a pure calculation for the same props, state, and context:
function Price({ amount, taxRate }) {
const total = amount + amount * taxRate;
return <strong>{total.toFixed(2)}</strong>;
}
Do not do this:
let renderCount = 0;
function Price({ amount }) {
renderCount += 1; // external mutation during render
localStorage.setItem('last-price', String(amount)); // side effect
return <strong>{amount}</strong>;
}
The second version can behave unexpectedly with Strict Mode, interrupted rendering, server rendering, or future compiler optimizations. The render may be evaluated more than once, or evaluated without ever being committed, so its external actions cannot safely be tied to the act of calculating JSX.
React calls components
Do not call component functions as ordinary functions:
// Wrong
const row = TaskRow({ task });
// Correct
const row = <TaskRow task={task} />;
React must own component invocation so it can associate Hooks, state, identity, errors, Suspense, and scheduling with the correct fiber in the tree.
React 19.2 and the modern baseline
This module assumes modern function components and React 19.2 behavior. It does not teach legacy lifecycle APIs as the default. Class components appear later only when understanding an Error Boundary or working in an older codebase requires them.
React 19.2 includes concepts such as useEffectEvent and <Activity>. They are introduced only after state, Effects, and transitions, because using a new API without the underlying model produces memorized code rather than an understanding of React.
React Compiler changes optimization strategy
React Compiler is stable and can automatically memoize components and values. That does not make memo, useMemo, or useCallback wrong; it changes when and why you reach for them. The course follows this order:
- write pure components;
- keep state close to where it is needed;
- avoid unnecessary Effects;
- measure with React DevTools;
- let the compiler optimize where enabled;
- use manual memoization when profiling or library constraints justify it.
Do not scatter memoization across beginner code.
Debugging render problems
When a component appears to “render too much,” investigate before optimizing:
- What state or context update scheduled the render?
- Did a parent render and therefore call this child again?
- Is the render actually expensive?
- Is state stored too high in the tree?
- Is an Effect creating a state-update loop?
- Is a key causing remounting rather than rerendering?
- Is development Strict Mode exposing an impurity?
A rerender is not automatically a performance bug. A remount, stale state, duplicated source of truth, or expensive render can be one.
Checkpoint
You should be able to explain why each of these is different:
- rendering a component again;
- committing DOM changes;
- remounting a component because identity changed;
- running an Effect after commit;
- hydrating server-rendered HTML;
- suspending while a dependency is not ready.
These distinctions provide the foundation for the rest of the React module.
Deep dive: reconciliation, identity, and why React can update selectively
React does not compare HTML strings. It compares the element tree from the previous render with the element tree from the next render, then decides which committed host nodes can be reused.
Consider this conditional tree:
function App({ loggedIn }) {
return (
<main>
<h1>Task Board</h1>
{loggedIn ? <Dashboard /> : <Login />}
</main>
);
}
When loggedIn changes, <main> and <h1> can keep their identity while the child at the conditional position changes from Login to Dashboard. React removes the previous subtree and mounts the new one.
This matters because state belongs to a component’s identity in the rendered tree. React roughly asks:
same position?
same component type?
same key?
If the answer changes, the previous state can be discarded.
Rerender is not remount
These two operations are different:
rerender
→ React calls the component again
→ existing state can be preserved
→ DOM may or may not change
versus:
remount
→ previous component identity is removed
→ cleanup runs
→ state is discarded
→ fresh state is created
A developer who confuses them often reaches for useEffect to “reset state” when the real issue is component identity or keys.
A small identity experiment
function Counter({ label }) {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((n) => n + 1)}>
{label}: {count}
</button>
);
}
function Demo() {
const [mode, setMode] = useState('a');
return (
<>
<button onClick={() => setMode((m) => (m === 'a' ? 'b' : 'a'))}>
Switch mode
</button>
{mode === 'a'
? <Counter label="A" />
: <Counter label="B" />}
</>
);
}
Because the same component type occupies the same position, React may preserve the Counter state when the branch changes.
If each mode should have a fresh counter, make the identities distinct:
{mode === 'a'
? <Counter key="a" label="A" />
: <Counter key="b" label="B" />}
The keys now explicitly declare two different identities.
Deep dive: render purity and restartability
Modern React can prepare work, pause it, resume it, or abandon it. Render code therefore has to be pure.
Unsafe:
let nextId = 0;
function Row() {
nextId += 1;
return <li id={`row-${nextId}`}>...</li>;
}
If React renders and later abandons that work, the global counter has still changed even though nothing was committed.
Safer:
function Row({ id }) {
return <li id={`row-${id}`}>...</li>;
}
Alternatively, use a React-supported identity API such as useId when its semantics fit the problem.
Render-time side effects that often sneak into real projects
Avoid these operations during render:
localStorage.setItem(...)
fetch(...)
socket.send(...)
analytics.track(...)
document.title = ...
element.focus()
new ThirdPartyWidget(...)
Some belong in event handlers, some in Effects, and some in a data or router layer. The boundary to keep clear is that rendering itself should only calculate the next UI description.
Deep dive: React's responsibilities versus framework responsibilities
React gives you primitives for:
- components;
- state;
- context;
- refs;
- Effects;
- concurrency;
- Suspense;
- server rendering primitives;
- Server Component primitives.
React alone does not prescribe the architecture for an entire application.
A production stack may additionally need:
routing
server-state cache
forms
validation
authentication
authorization
build tooling
SSR/RSC framework
testing
monitoring
This course teaches React’s mental model first and the surrounding ecosystem afterward. If you learn libraries before you understand state ownership and rendering semantics, every library API looks like magic.
Failure clinic
"React rerendered, so it must have changed the DOM"
False. A component can rerender and produce exactly the same host output.
"More rerenders always means poor performance"
False. Cheap pure rerenders are often fine. Measure actual expensive commits and browser work.
"State belongs to a component function"
Not exactly. State is associated with a component’s identity in the rendered tree. The same function can appear multiple times, and each instance receives independent state.
"React is the virtual DOM"
That description is too narrow. Reconciliation matters, but modern React also includes scheduling, Suspense, transitions, server rendering, Server Components, Actions, and compiler-driven optimization.
Debug lab
Build this intentionally broken component:
let renders = 0;
function BrokenProfile({ user }) {
renders += 1;
localStorage.setItem('lastUser', user.id);
return (
<section>
<h2>{user.name}</h2>
<p>Render #{renders}</p>
</section>
);
}
Then:
- enable Strict Mode;
- trigger parent rerenders;
- observe that
rendersis not a reliable committed-render count; - move storage synchronization to an Effect or event depending on the requirement;
- remove the global render counter;
- use React DevTools Profiler if you actually need render evidence.
The lesson is not simply “Strict Mode renders twice.” The broader lesson is that render code must tolerate React checking, replaying, or abandoning work.
Mastery check
Before moving on, you should be able to explain, without vague phrases:
- what causes a render;
- what render produces;
- what commit means;
- when state is preserved;
- when state resets;
- why keys affect identity;
- why side effects do not belong in render;
- why a rerender is different from a DOM mutation;
- why React’s scheduling model requires purity.
