The pain of manual DOM updates
This course assumes two things from earlier courses. From Web Development Fundamentals you know what the DOM is: the browser's live tree of elements that JavaScript can read and change. From Advanced JavaScript you know functions, arrow functions, destructuring, and array methods like map and filter. We will lean on all of them.
React is a JavaScript library for building user interfaces. Before we look at any React code, it is worth feeling the problem it solves, because every React idea makes sense once you see the pain it removes.
The old way: update the DOM by hand
In Web Development Fundamentals you updated pages like this:
const count = document.getElementById("count"); const button = document.getElementById("add"); let clicks = 0; button.addEventListener("click", () => { clicks = clicks + 1; count.textContent = String(clicks); if (clicks >= 10) { count.classList.add("warning"); } });
This is called imperative code: you spell out every DOM change yourself. It works fine for one counter. Now imagine 40 pieces of the page all depend on clicks, plus a filter dropdown, plus a logged-in user. Every event handler must remember to update every affected element. Forget one, and the screen no longer matches the data. That mismatch is the classic source of UI bugs.
React's big idea: describe, don't patch
React flips the model. Instead of writing instructions for how to change the screen, you write a function that describes what the screen should look like for the current data. When the data changes, React calls your function again and updates only the DOM pieces that differ.
This style is called declarative: you declare the result, React performs the steps. Your job shrinks to two things:
- Keep your data correct.
- Describe the UI for any possible data.
The screen can never drift out of sync with the data, because the screen is always recomputed from the data.
The sync bug, live
addToCartBuggy updates the data and the badge and forgets the checkout line, which is the exact mistake that slips into real handlers. The declarative version at the bottom recomputes every dependent string from the data in one place, so it cannot forget.
let clicks = 0; let badge = "Cart (0)"; let checkout = "Checkout 0 items"; function addToCartBuggy() { clicks = clicks + 1; badge = "Cart (" + clicks + ")"; // forgot to update checkout } addToCartBuggy(); console.log(badge); console.log(checkout); // declarative fix: recompute EVERY dependent string from the data function render(count) { return "Cart (" + count + ") | Checkout " + count + " items"; } console.log(render(clicks));
Output
Cart (1) Checkout 0 items Cart (1) | Checkout 1 items
The second line prints the stale checkout text, so the data says 1 item and the screen says 0. Nothing crashed and no error appeared, which is what makes this class of bug so expensive. The page simply lies, and a user is the one who notices.
render never patches anything. It answers one question, which is what the screen should say for this count, and that is the declarative style React uses.
Count the places the bug could hide. With three dependent strings there are three chances to forget on every handler, and a real page has dozens of dependent elements and dozens of handlers, so the number of chances is the product of the two. The declarative version has exactly one place where the mapping from data to screen is written.
Note that the buggy version and the fixed version hold the same data. Nothing about clicks was wrong, which is the point of the equation coming in the next lesson: keep the data right, and derive everything else from it rather than maintaining copies by hand.
What every handler has to do the manual way
On a page where a cart icon, a cart page, and a checkout button all display the item count, the manual DOM approach requires each "add to cart" handler to update the data and remember to patch all three DOM places that show the count.
The DOM does not watch your variables. In imperative code, each handler must find and update every element that depends on the changed data, and missing one leaves the screen stale.
The work grows in the worst possible way, since it is handlers multiplied by dependent elements rather than either one alone:
| Page | Dependent elements | Handlers that change the count | Places to remember |
|---|---|---|---|
| one counter | 1 | 1 | 1 |
| small cart | 3 | 3 | 9 |
| real store | 12 | 8 | 96 |
Every one of those 96 is a chance to forget, and each omission produces a screen that disagrees with the data in a way no test of the data alone can catch.
React removes the burden by recomputing the UI from the data, which turns the table's last column into 1 no matter how the first two grow.
Defining declarative UI
Declarative UI means you describe what the screen should look like for the current data, and React figures out the DOM changes.
You state the desired result rather than the steps to reach it. Your code is a description of the UI as a function of data, and React computes the minimal DOM edits needed to make the real page match that description.
Imperative is the opposite, being step-by-step patch instructions, which is what count.textContent = String(clicks) is. That line says how to change the screen, and it says nothing about what the screen ought to show in general.
The distinction is not unique to UI work. for loops that push into an array are imperative, while map and filter from Advanced JavaScript are declarative, since they say what the new array should contain rather than how to build it up. React applies the same shift to the screen.
Two things follow from the definition and are worth stating plainly. Your code no longer contains the words document.getElementById, and the screen cannot drift out of sync with the data, because it is recomputed from the data every time.