Goal: understand React as a declarative UI library, where the interface is a function of the current props and state, and be able to explain what happens between a click and an updated screen.
Almost every confusing moment in React, from “why didn’t my variable update?” to “why does this run twice?”, comes from thinking about it the way we think about plain DOM scripting. This first lesson replaces that model with the one React is built on. Everything else in the course, and every feature of the tool library you are about to build, rests on it.
In this lesson
Imperative vs declarative UI
Every tool in the library has a Reserve button. With plain JavaScript you write imperative code: you find the elements and tell the browser, step by step, what to change when the button is clicked:
const button = document.querySelector('#reserve');
const status = document.querySelector('#status');
let reserved = false;
button.addEventListener('click', () => {
reserved = !reserved;
status.textContent = reserved ? 'Reserved for you' : 'Available';
button.textContent = reserved ? 'Cancel reservation' : 'Reserve';
button.classList.toggle('is-active', reserved);
});
Three DOM updates for one change of data. Now add the real rules: a tool in repair cannot be reserved, a member may hold at most three tools, the header shows how many you have reserved. Each rule is another instruction that must remember to update every affected element, in every handler. Miss one and the screen says something your data doesn’t.
React is declarative: you describe what the UI should look like for any value of the data, and React works out which DOM changes are needed. The same button in React:
import { useState } from 'react';
function ReserveButton() {
const [reserved, setReserved] = useState(false);
return (
<div>
<p>Cordless drill: {reserved ? 'Reserved for you' : 'Available'}</p>
<button onClick={() => setReserved(!reserved)}>
{reserved ? 'Cancel reservation' : 'Reserve'}
</button>
</div>
);
}
There is no querySelector, no textContent and no classList. The component states, once, what the tool looks like when it is reserved and when it is not. Press Try it and add the first rule yourself: add const inRepair = true; at the top of the component, then give the button disabled={inRepair} and show “In repair” instead of “Available” when it is. You add expressions that describe the result, not another event listener.
UI as a function of state
The short version of React’s model is UI = f(state). A component is a function. Its inputs are props (data passed in by its parent; Lesson 4) and state (data it remembers between renders; Lesson 5). Its output is a description of the UI, written in JSX (Lesson 2). Given the same inputs, it should return the same description.
That changes the question you ask when building a feature. Instead of “what do I need to change when the user clicks?”, you ask two simpler questions:
- What data does this screen depend on? For a tool card: its name, whether it is reserved, whether it is in repair. That becomes props and state.
- What should the screen look like for each value of that data? That becomes the JSX.
Events don’t change the screen directly. They change data, and the screen follows.
From click to screen: trigger, render, commit
When you click Reserve in the example above, three things happen in order:
- Trigger. The click handler calls
setReserved(true). Nothing on screen changes yet; this asks React to schedule a new render with the new value. - Render. React calls
ReserveButton()again. This timeuseStatereturnstrue, so the function returns JSX describing “Reserved for you” and a “Cancel reservation” button. Rendering is just calling your function; nothing touches the page in this step. - Commit. React compares the new description with the previous one and applies only the differences to the DOM: here, two pieces of text. The browser then paints the result.
Keep render and commit separate in your head. Render is where your component code runs, and it should only calculate. Commit is where React changes the DOM. Side effects such as network requests or timers belong in event handlers, or in effects when they keep something outside React in sync (Lesson 9), never in the middle of a render.
Renders should be pure
React decides when to call your components, and may call them more often than you expect: whenever a parent re-renders, and in development twice in a row on purpose when Strict Mode is on, to expose this exact kind of bug. So a render must be a pure calculation: read the inputs, return JSX, and change nothing that existed before the call.
Here is a catalogue that tries to count available tools by having each row add to a shared counter as it renders. Before you run it, predict the number at the bottom:
let availableCount = 0;
function ToolRow({ name, available }) {
// Impure: changes a variable that lives outside the component.
if (available) {
availableCount = availableCount + 1;
}
return <li>{name}: {available ? 'available' : 'out on loan'}</li>;
}
function App() {
return (
<>
<ul>
<ToolRow name="Cordless drill" available={true} />
<ToolRow name="Extension ladder" available={false} />
<ToolRow name="Orbital sander" available={true} />
</ul>
<p>Available now: {availableCount}</p>
</>
);
}
It says 0, although two tools are available. App runs first and builds its JSX, reading availableCount while it is still 0; the rows only render afterwards. And on every later re-render the rows would add to the old total, so the number would drift further from the truth. The result depends on when and how often React renders, which is exactly what a pure component must never depend on.
The fix is to stop counting as a side effect and calculate the number from the data during render:
const tools = [
{ id: 't1', name: 'Cordless drill', available: true },
{ id: 't2', name: 'Extension ladder', available: false },
{ id: 't3', name: 'Orbital sander', available: true },
];
function ToolRow({ tool }) {
return <li>{tool.name}: {tool.available ? 'available' : 'out on loan'}</li>;
}
function App() {
const availableCount = tools.filter((tool) => tool.available).length;
return (
<>
<ul>
{tools.map((tool) => <ToolRow key={tool.id} tool={tool} />)}
</ul>
<p>Available now: {availableCount}</p>
</>
);
}
Now the count is always right, however many times React renders. (Rendering a list with map and key gets a full lesson in Lesson 7.) Creating and changing variables inside a render is fine, because they belong to that one call. The rule is about things that outlive it: outside variables, objects received as props, the DOM, storage and the network.
State is a snapshot
Inside a single render, a state variable is a fixed value, like a photo of the data at that moment. Calling the setter does not change that variable; it schedules the next render with a new value. A loan starts at 7 days, and a member wants to extend it by two weeks. Predict what this shows after one click, then run it:
import { useState } from 'react';
function LoanLength() {
const [days, setDays] = useState(7);
function extendByTwoWeeks() {
setDays(days + 7);
setDays(days + 7);
console.log('days during this click:', days);
}
return (
<div>
<p>Loan length: {days} days</p>
<button onClick={extendByTwoWeeks}>Extend by two weeks</button>
</div>
);
}
The loan becomes 14 days, not 21, and the log shows the old value, 7. In that render days is 7, so both calls say “set it to 7 + 7”. When the next value depends on the previous one, pass a function instead: setDays((d) => d + 7). React then applies each update to the result of the one before. Change both lines to that form and run it again. Lesson 5 goes deeper into updating state.
Common mistakes
- Changing React-managed DOM by hand. Calling
document.querySelectorand settingtextContentor classes on elements React renders. React doesn’t know about the change and will overwrite it on the next render. Change state instead and let the JSX reflect it. (Refs, in Lesson 9, cover the few legitimate cases such as focusing an input.) - Expecting state to update immediately. Reading a state variable straight after calling its setter gives the old value, as the loan example shows. Use the value you are about to set, or the updater function form.
- Side effects during render. Fetching data, starting timers, writing to
localStorageor counting into outside variables in the component body. They run on every render, possibly twice in development. Put them in event handlers, or in an effect when they synchronise with something outside React.
In code review, the quickest check is to read a component’s body and ask: “If React called this ten times in a row with the same props and state, would anything outside it change?” If yes, something needs to move.
Build it: the repair switch
Volunteers need to take broken tools out of circulation. Build a RepairSwitch component for the orbital sander:
- It shows “Orbital sander: Available” and a button “Mark as in repair”.
- Clicking the button changes the status to “In repair” and the button to “Mark as repaired”.
- While the tool is in repair, a note appears: “Members can’t reserve this tool until it is repaired.” It must not be on the page at all otherwise.
Then write down, in three short steps, what happens from the click to the updated screen, using the words trigger, render and commit.
import { useState } from 'react';
function RepairSwitch() {
// Your code here: one piece of state is enough.
return (
<div>
<p>Orbital sander: Available</p>
<button>Mark as in repair</button>
</div>
);
}
Hint: you don’t need separate state for the status text, the button label and the note. All three can be calculated from one boolean. To show something only sometimes, {condition && <p>…</p>} renders the paragraph when the condition is true and nothing when it is false.
Show a sample solution
import { useState } from 'react';
function RepairSwitch() {
const [inRepair, setInRepair] = useState(false);
return (
<div>
<p>Orbital sander: {inRepair ? 'In repair' : 'Available'}</p>
<button onClick={() => setInRepair(!inRepair)}>
{inRepair ? 'Mark as repaired' : 'Mark as in repair'}
</button>
{inRepair && (
<p>Members can't reserve this tool until it is repaired.</p>
)}
</div>
);
}
One piece of state (inRepair) drives everything; the status, the label and the note are all derived from it during render, so they can never disagree. The sequence: the click handler calls setInRepair(true) (trigger); React calls RepairSwitch() again and gets JSX with the new status, label and note (render); React updates the two changed texts and inserts the note into the DOM (commit).
Check your understanding
Answer each question in a sentence or two before opening the answer.
1. What does “UI = f(state)” mean in practice?
A component describes what the screen should look like for its current props and state. To change the screen you change the data, and React re-runs the component and updates the DOM to match. You never instruct the DOM directly for ordinary updates.
2. Why does logging a state variable right after calling its setter show the old value?
State is a snapshot for the current render. The setter schedules a new render with the new value; it does not change the variable in the render that is already running. The new value is there the next time the component runs.
3. A tool card calls fetch() in its body to load the tool’s reviews. What goes wrong, and how would you notice?
The request runs on every render of the card, including the extra development render in Strict Mode and every re-render caused by a parent, so it fires many times and the responses can arrive out of order. Open the browser’s Network panel while using the page and you will see the duplicate requests. Load data in an event handler, in an effect with cleanup (Lesson 9), or, in Next.js, in a Server Component (Lesson 27).
Key takeaways
- React is declarative: describe the UI for the current data and let React update the DOM.
- An update goes trigger (set state), render (React calls your component), commit (React changes the DOM).
- Renders must be pure: same props and state in, same JSX out, nothing outside changed. Calculate values like counts from the data instead of accumulating them.
- State is a snapshot. Setters schedule the next render; use
setX((prev) => …)when the new value depends on the old one.
Priority: must understand. You should be able to explain this model, build a small example and spot a component that breaks it in someone else’s code. For the official explanation of rendering, see Render and Commit and Keeping Components Pure in the React documentation.
