The problem
Writing a page by hand means writing every transition: when the count changes, find the element, update its text, toggle a class, maybe remove a row. Each new piece of state multiplies the number of transitions you have to get right.
let items = [];
let filter = "";
function addItem(text) {
items.push({ id: nextId++, text, done: false });
const li = document.createElement("li");
li.textContent = text;
list.append(li);
count.textContent = items.length;
empty.hidden = items.length > 0;
if (filter && !text.includes(filter)) li.hidden = true;
}
function toggle(id) {
const item = items.find((i) => i.id === id);
item.done = !item.done;
document.querySelector(`[data-id="${id}"]`).classList.toggle("done", item.done);
count.textContent = items.filter((i) => !i.done).length; // …and so on
}
Every function has to remember every consequence. Miss one and the screen disagrees with the data — the bug that is hardest to find, because nothing is broken except what you can see.
The idea
React turns it around. You write one function from state to markup, and when the state changes React calls it again and updates whatever actually differs.
function List({ items, filter }) {
const shown = items.filter((i) => i.text.includes(filter));
return (
<>
<p>{shown.filter((i) => !i.done).length} left</p>
{shown.length === 0 && <p>Nothing here yet.</p>}
<ul>
{shown.map((item) => (
<li key={item.id} className={item.done ? "done" : ""}>{item.text}</li>
))}
</ul>
</>
);
}
There is no addItem that has to remember the counter, the empty state and the filter. There is one description of what the screen looks like for any state, and React works out the difference.
What that buys you
- The screen cannot disagree with the data. It is computed from it.
- A new feature is a new expression, not a new set of transitions in five functions.
- Components compose. A list of cards is a
Cardused many times, with its own state. - You stop querying the DOM. Almost no
querySelectorin a React codebase, and no ids to keep in step.
What it costs
- A build step and a dependency, where a plain page needs neither.
- About 45 KB of JavaScript before your own code.
- A new set of rules to learn — hooks, keys, dependencies, re-renders — and new ways to be wrong.
- Everything happens after JavaScript loads, unless you add a framework that renders on the server.
When it is not worth it
A marketing page, a blog, a form, a documentation site: these have little state and a lot of content. Plain HTML with a few hundred lines of JavaScript is faster to build, faster to load and easier to keep working.
React earns its cost when the same data appears in several places at once, when state changes often, and when the interface is assembled from pieces that repeat — a dashboard, an editor, an application.
Count the pieces of state and the places each one appears. One or two, in one place each: write it by hand. A dozen, appearing in several places: that is what React is for.
What React is not
- Not a framework. It renders components. Routing, data fetching, forms and building are separate choices — which is why Next.js exists.
- Not fast by default. It is fast enough, and it makes the slow thing (manual DOM work) unnecessary. A badly written React app is slower than a well-written plain one.
- Not a way to avoid learning the DOM. Everything in the HTML, CSS and JavaScript courses still applies — React sits on top of it, and when something is wrong you debug the DOM.
The mental model
Everything else in this course is a detail of those three sentences.
// 1. A component is a function that returns what to show.
function Badge({ count }) {
return <span className="badge">{count}</span>;
}
// 2. Props come in from the parent. They are read-only.
<Badge count={3} />
// 3. State belongs to a component. Changing it re-runs the function.
const [count, setCount] = useState(0);
setCount(count + 1); // React calls the component again