088: Why React?
Learning objective
Outcomes
React is a way to organize UI code, not magic. After working through this material, you should be able to compare imperative DOM updates with declarative rendering, identify useful component boundaries, define state, and describe a re-render without claiming that React reloads the page.
I can explain what problem React solves and sketch a component tree for a task manager.
Prerequisites
You should be comfortable with the completed JavaScript module (042–087): functions, arrays, objects, modules, DOM events, forms, Promises, fetch, and HTTP/API basics. If DOM selection, form submission, or async request handling are unfamiliar, review those JavaScript topics before continuing.
Retrieval practice
Before reading, answer:
- 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
Imperative code says how to perform each DOM operation: find this element, remove that class, change this text. Declarative code describes what the UI should look like for current data. React calls component functions to calculate JSX, then commits the necessary DOM changes.
- Component: A JavaScript function returning 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
Imagine a spreadsheet. You edit an input cell, formulas recalculate, and displayed cells update. You describe relationships rather than manually repainting every dependent cell. React similarly derives UI from data. Unlike a spreadsheet, your component functions must stay pure during rendering: same inputs, same JSX, with no DOM mutation or network request in the component body.
Imperative versus declarative
An imperative vanilla task count can drift out of sync:
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`;
}
Every operation must remember all affected DOM. Add filtering, deletion, editing, and loading states and there are many synchronization paths. React instead encourages this relationship:
function TaskSummary({ tasks }) {
return <p>{tasks.length} tasks</p>;
}
If tasks changes, the next render calculates the correct count. React does not eliminate complexity: the application still needs good data and update rules. It centralizes the relationship between data and visible output.
Beginner complete example
For now, use the React playground at react.dev or place this in a Vite src/App.jsx later. It is complete as one component file and intentionally has 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 exists independently from the markup. App composes TaskList, and TaskList creates one TaskItem per task. The open count is derived from the same array, so there is one source of truth.
Intermediate: identify boundaries
Start from user and developer needs, not a rule that every div deserves a component. 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 benefits from independent testing. Keep closely related markup together. A TaskTitleTextWrapper component would hide rather than clarify intent.
Where should changing task data live? A useful rule is to place 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. Later it passes data down and event callbacks down; children do not reach sideways into siblings.
Optional advanced: what React actually updates
Calling a state setter queues a render. React calculates a new render tree and compares identity, element type, keys, and props with the previous tree. During commit, it performs necessary DOM operations. A parent render can call child components again even if their DOM output remains unchanged. Therefore, “re-render” means recalculation, not “rewrite the whole DOM.” Do not add useMemo or useCallback merely because a component renders; measure a real performance problem first. Current React tooling, including the React Compiler where configured, further reduces the case for speculative manual memoization.
Mistakes and debugging
- Treating React as a templating language only: its useful model includes components, state, events, and predictable data flow.
- Mutating DOM managed by React: direct
querySelector(...).textContent = ...can conflict with the next commit. Describe changes through state. - 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.
Debug by asking: What data produced this screen? Which component owns it? Was the source mutated? Is the displayed value derivable? React Developer Tools can inspect component props and state; browser DevTools inspects the resulting DOM.
Accessibility and performance
React does not make inaccessible markup accessible. Prefer semantic <main>, headings in order, real <button> controls, <form>, labels, and list elements. Do not make a clickable div. State changes that communicate loading or errors may need suitable text and live-region behavior, covered later.
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 copy data and avoid manual memoization without evidence. The browser still downloads JavaScript before this client UI runs, 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 of the form, filters, list, and summary. 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. It should not be nested in every task unless each item independently owns 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 output from state. Event handlers change the state; rendering chooses the matching row.
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; React then commits only needed DOM changes. Components are useful responsibility boundaries, not wrappers for every element. Keep rendering pure and 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 React update normally has a render phase, where components calculate the next element tree, and a commit phase, where React applies necessary host-tree changes and runs relevant effects. A component function running again does not mean that 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 preservation rather than memorizing 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 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 to the previous tree. Same type and stable key generally preserve the component instance; a changed key deliberately resets it. Keys do not make rendering faster by themselves and are not passed 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 the child produces identical DOM.
Referential equality uses Object.is: equal-looking objects and functions are different references. This failure case defeats shallow comparisons and can retrigger Effects:
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)} />;
}
Keep state local and pass primitives where practical before memoizing. 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 makes impure rendering safe or blocks changed context, and each adds comparison/dependency 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 contract: click Increment and expect Count: 1; click Same value and expect Count: 0. Use React DevTools Profiler for render evidence rather than brittle render-count assertions, because Strict Mode and scheduling affect development calls. Interview follow-ups: what schedules work without changing the DOM, why can a parent call a child again, and when is state locality better than memo?
2026 depth expansion: React's actual execution contract
As of React 19.2, the most useful mental model is not “React updates the DOM.” It is:
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 be started and later abandoned. This is why mutating a module variable, writing to storage, starting a request, or changing the DOM during render is a correctness bug rather than merely a style preference.
Purity is an architectural requirement
A component should behave 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.
React calls components
Do not call component functions as ordinary functions:
// Wrong
const row = TaskRow({ task });
// Correct
const row = <TaskRow task={task} />;
React needs to 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 are only introduced later where understanding an Error Boundary or an older codebase requires them.
React 19.2 adds concepts such as useEffectEvent and <Activity>. These are taught only after state, Effects, and transitions because using a new API without the underlying model produces memorized code rather than React understanding.
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 their role. The course therefore 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 “renders too much,” first ask:
- 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.
Checkpoint
You should be able to explain why all of the following are 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 are 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 produced by the previous render with the element tree produced by the next render and decides which committed host nodes can be reused.
Consider:
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. The previous subtree is removed and the new subtree is mounted.
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 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 the two 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 across the branch change.
If you intend a fresh counter per mode:
{mode === 'a'
? <Counter key="a" label="A" />
: <Counter key="b" label="B" />}
Now the key declares a different identity.
Deep dive: render purity and restartability
Modern React can prepare work, pause, resume, or abandon it. This is why render code must be pure.
Unsafe:
let nextId = 0;
function Row() {
nextId += 1;
return <li id={`row-${nextId}`}>...</li>;
}
If React renders and later abandons that render, the global counter still changed even though nothing was committed.
Safer:
function Row({ id }) {
return <li id={`row-${id}`}>...</li>;
}
or use a React-supported identity API such as useId where its semantics fit.
Render-time side effects that often sneak into real projects
Avoid these during render:
localStorage.setItem(...)
fetch(...)
socket.send(...)
analytics.track(...)
document.title = ...
element.focus()
new ThirdPartyWidget(...)
Some of these belong in events, some in Effects, and some in a data/router layer. The important point is that rendering itself should only calculate the next UI description.
Deep dive: React's responsibilities versus framework responsibilities
React itself gives you primitives for:
- components;
- state;
- context;
- refs;
- Effects;
- concurrency;
- Suspense;
- server rendering primitives;
- Server Component primitives.
React alone does not decide your whole application architecture.
A production stack may additionally need:
routing server-state cache forms validation authentication authorization build tooling SSR/RSC framework testing monitoring
This course deliberately teaches React's mental model first, then the surrounding ecosystem. If you learn libraries before 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 may rerender and produce 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 gets independent state.
"React is the virtual DOM"
Too narrow. Reconciliation is important, 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 "Strict Mode renders twice." The 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 using 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.
