You can build many things in React without knowing how it works inside. But when something does not behave as you expect, a clear picture of how React draws will save you hours. This lesson explains rendering in simple steps, and the rules that make React code predictable.
What does “render” mean?
In React, rendering means React calls your component function to find out what should be on the screen. It is not the same as drawing pixels. Rendering is only the thinking part. The function returns JSX, which is a description of the screen.
A render and the following update happens in three steps:
- Trigger: something asks for a draw. This is either the first draw of the app, or a state change.
- Render: React calls your component (and the components inside it) and gets their JSX.
- Commit: React compares the new result with the old one, and changes only the differences in the real page.
If the render result is the same as last time, React changes nothing on the page, even though your function ran.
What causes a re-render?
- A component's own state changes (a setter is called with a different value).
- Its parent renders. By default, when a component renders, all the components inside it render too, even if their props did not change.
- A context it reads changes.
That second point surprises people. React renders children by default because it is usually cheap, and because checking every component would also cost time. If a render is truly slow, you can reduce it with memo, which lesson 21 covers. Do not worry about it until you see a real problem.
Pure components
The key rule of React: a component must be a pure function of its props and state. “Pure” means:
- Same inputs, same output. With the same props, state and context, a component always returns the same JSX.
- No side effects while rendering. Rendering must not change anything outside the component, such as a global variable, the page, or a server.
Compare these two:
// Impure: changes something outside, and gives a different result on each draw
let count = 0;
function Bad() {
count = count + 1;
return <p>Draw number {count} at {Date.now()}</p>;
}
// Pure: depends only on its props
function Good({ count }) {
return <p>Count is {count}</p>;
}
React may call your component more than once, may pause a draw, or may throw one away and start again. This only works if rendering has no hidden effects. The place for effects is event handlers and `useEffect`, as we saw before.
Local changes are fine
A component may change things it created itself during the same render. For example, building a new array and filling it is fine:
function Names({ people }) {
const names = []; // made during this render
for (const person of people) {
names.push(person.name); // fine
}
return <p>{names.join(', ')}</p>;
}
What is not fine is changing the props or state, or things made before this render.
Immutability: never change, always replace
React finds out whether a value changed by checking if it is a new object. So we must not change arrays and objects in state or props. We make a new copy. This is the reason for [...items, newItem] and { ...user, name: 'x' }.
// Wrong: same array object, React sees no change
items.push('x');
setItems(items);
// Right: a new array
setItems([...items, 'x']);
For deep objects, copying every level by hand gets long. A small library called Immer (used inside Redux Toolkit and available as useImmer) lets you write “changes” in a safe way. Learn the plain way first.
State is a snapshot
Each render has its own copy of the state, like a photo taken at that moment. Inside a render, and inside the event handlers made during it, the values do not change, even if you call a setter. That is why this prints the old number, and why three setCount(count + 1) calls add only one:
function Demo() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // asks for 1
setCount(count + 1); // asks for 1 again, not 2
setCount(count + 1); // still 1
console.log(count); // 0, the snapshot of this render
}
return <button onClick={handleClick}>{count}</button>;
}
The fix is the function form: setCount((c) => c + 1), which works on the latest value instead of the snapshot.
Batching: many updates, one render
If you call several setters in one event, React waits until the handler finishes, then renders once with all the new values. This is called batching. It makes your app faster, and since React 18 it works everywhere, even inside timers and after fetch.
function handleSubmit() {
setName(''); // no render yet
setEmail(''); // no render yet
setSent(true); // now React renders once with all three changes
}
So you do not need to worry about “too many renders” when you set a few states together.
Infinite loops
If you set state during a render, for example setCount(count + 1) directly in the body of the component, React renders again, which sets state again, and so on forever. React stops it with an error: “Too many re-renders”. State must be set in event handlers or effects, not while rendering. The same happens with an effect that sets state and has no dependency array.
StrictMode: your safety net
StrictMode is the wrapper you saw in main.jsx. In development only, it:
- Renders each component twice to find impure code. If your component prints something or changes a variable during render, you will see the double result.
- Runs each effect, then its cleanup, then the effect again, to test that cleanup works.
This is on purpose, to help you. It does nothing in the built site, so it costs your visitors nothing. If you see something happen twice in development, check whether your code is pure and has a cleanup, instead of removing StrictMode.
Keys and identity
React remembers state by the position of a component in the tree, and by its key. If a component stays in the same place, it keeps its state. If it moves to a different place, or you give it a different key, React throws away the old one and makes a new one. This gives you a nice trick: to reset a component's state, change its key.
<ProfileForm key={userId} userId={userId} /> {/* a new form for each user */}
It also explains a bug: defining a component inside another component makes a brand new component type on every render, so its state is lost each time. Always define components at the top level of the file.
A simple way to debug renders
- Add
console.log('Card render')at the top of a component and watch how often it runs. - Use the Profiler tab in React Developer Tools to record a session and see what rendered and how long it took.
- Ask: what triggered this? (state, parent, context). Then ask: is the work slow enough to matter? Most of the time it is not.
Common mistakes
- Changing a variable outside the component during render.
- Changing state directly instead of making a new copy.
- Setting state in the body of the component, which causes a loop.
- Defining components inside components.
- Worrying about re-renders before measuring a real problem.
Your task
- Add a
console.login a parent and a child component. Click a button in the parent that changes its state, and watch which components log. - Break purity on purpose: change a global variable in a component, and see the double change in development.
- Make a
Profileform that resets when you change itskey. - Click a button that calls
setCount(count + 1)three times, then fix it with the function form.
How to check it worked
- You can explain trigger, render and commit in your own words.
- You know why a child renders when its parent renders.
- The count increases by 3 after you use the function form.
Summary
- Rendering means calling your component. React then compares results and changes only the differences in the page.
- A re-render is caused by state, a parent render, or a context change. It is normal and usually cheap.
- Components must be pure: same inputs give the same output, with no side effects while rendering.
- Never change state or props. Make copies. State is a snapshot per render, and updates are batched.