Topic 16.1
Why Components Re-render
In one line
A component renders again for three reasons only: its own state changed, its parent rendered, or a context it reads changed; rendering means calling your function, which is cheap, and React touches the DOM only where the result is different.
Think of it like this
A teacher asks the class "is anything different in your homework since yesterday?". Every child checks their own notebook (that's rendering), but only the child whose answer changed walks to the board and rewrites it (that's updating the DOM). Asking is quick. Rewriting the board is the slow part, and React only does it where something really changed.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Render
- React calling your component function to get a description of the screen. No DOM is touched yet.
- Re-render
- Any render after the first one, caused by a state, parent or context change.
- Commit
- React applying the differences it found to the real DOM.
- Subtree
- A component and everything inside it: its children, their children, and so on.
- Object.is
- The comparison React uses to decide whether a state or context value changed. Like
===, with small differences forNaNand-0. - Bail out
- React deciding it can skip rendering a component (or its children) because nothing it depends on changed.
- Cost model
- A rough picture of what an update costs: JavaScript for rendering, then DOM work for the parts that changed.
Step by step
01Find who owns the state
When something on screen changes, first ask: which component called useState for this value? That component, and everything below it, is what renders. Everything above it, and its siblings' subtrees, stay as they are.
02Parents render their children
App returns <Title />. When App renders, React creates a new Title element and calls Title again, even though Title takes no props at all. Nothing is wrong: Title returns the same <h2>, React compares it and changes nothing in the DOM.
function App() {
const [count, setCount] = useState(0);
return (
<main>
<Title /> {/* renders whenever App does */}
<Display count={count} />
</main>
);
}03Setting the same value bails out
setCount(count) with the current value doesn't render the children. React may call App one extra time to be sure, throw that result away, and stop. Don't rely on the exact number of these extra calls: rely on components being pure (Topic 2.6), so extra calls are harmless.
04Context skips the middle
A context reader renders when the context value changes, even if its parent was skipped with memo. Context is a direct line from the provider to every reader.
05Rendering is not DOM work
After rendering, React compares results. If a <p> produces the same text and attributes, its DOM node isn't touched at all. You can prove it with a MutationObserver, a browser API that reports every DOM change, as the second live example does.
06Decide if it matters
Extra renders only matter when they cost time the user notices. Measure first (Topic 16.6). Usually the fix is structural: move state down to where it's used, or pass slow parts as children so they aren't re-created. memo and friends come after that.
Try it yourself
- 1
Move the state down
In "The three reasons, logged", predict what happens if
Displayowned the count itself (moveuseStateand the Add button intoDisplay). Which lines print on each click? Make the change and check:AppandTitlestop rendering. - 2
Remove the memo
In the context example, replace
memo(function Toolbar() {...})with a plainfunction Toolbar(). Predict the log for "change other state". Now Toolbar and ThemedButton render on every App render, yet the DOM count stays 0. - 3
Pass the slow part as children
Write a
Layout({ children })that owns its ownuseStatecounter and renders{children}below its button. Render<Layout><Title /></Layout>from App. Click the Layout's button: doesTitlerender? (No.Title's element was created by App, which didn't render, so React sees the same element object and skips it.)
Code & diagrams
Add one: App and both children render (reason 1, then reason 2). The first "same value" click calls App once more and throws the result away, without rendering the children. The second is skipped entirely.
Preview
Console
Nothing logged yet.Expected console output
App rendered
Title rendered
Display rendered, count = 0
--- click Add one
App rendered
Title rendered
Display rendered, count = 1
--- click Set the same value
App rendered
--- click Set the same value againToggling the theme skips Toolbar (memo, no props changed) but ThemedButton still renders: it reads the context. App rendered three times, yet the static <p> was never touched in the DOM. `<ThemeContext value>` as a provider is React 19 syntax (Topic 9.2).
Preview
Console
Nothing logged yet.Expected console output
App rendered
Toolbar rendered
ThemedButton rendered with light
--- toggle theme
App rendered
ThemedButton rendered with dark
--- change other state
App rendered
DOM changes inside the static <p>: 0Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Expect a ref change to update the screen
Store the count in const count = useRef(0) and do count.current++ in the click handler, showing {count.current} in the JSX.
Myth vs fact
Myth
A component re-renders when its props change.
Fact
It re-renders when its parent renders, whether or not the props changed. Comparing props is opt-in with memo.
Myth
Every re-render rewrites the DOM.
Fact
Rendering only calls your function. React changes a DOM node only when the new output for it differs. A thousand renders with the same output cost zero DOM work.
Myth
Re-renders are bad and should be avoided.
Fact
Re-renders are how React works. They matter only when they are slow enough for the user to notice. Measure before optimising.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The "same value" bail-out has two paths. If the component's fiber has no pending work, React compares the new state eagerly inside
setStateand doesn't even schedule a render. Otherwise it must render the component to compute the state and bails out of the children only after that render: the extraApp renderedline you saw. - ▸
The cheapest optimisation is composition. Elements passed in as
children(or any prop) were created by the owner component, so when an intermediate component re-renders with its own state, those elements are the same objects as before and React skips them withoutmemo. - ▸
Context has no selectors: a reader re-renders on any change to the value, even if it only uses one field. Splitting contexts, memoising the value, or moving to an external store with selectors (
useSyncExternalStore, Zustand, Topic 15.2) are the scaling answers. - ▸
In development, StrictMode calls every component twice per render (Topic 7.4), and the development build does extra checks. Never judge real speed from a development build: profile a production build.
Remember this
- 1
Rendering means React calling your component function to get a fresh description of the screen (React elements, Topic 4.8). A re-render is any render after the first one. Re-rendering is not a bug: it's how React finds out what the screen should look like now. A performance problem only exists when renders are so many, or so expensive, that the user feels it: typing lags, clicks feel slow, scrolling stutters.
- 2
Reason 1: state changed. Calling a state setter (
setCount(1)) ordispatchschedules a render of the component that owns that state. React compares the new value with the old one usingObject.is. If they are the same (setCount(count)), React usually skips the render entirely. One known detail: right after an update, React may still call the component once more before it can prove nothing changed. It throws that result away and does not render the children. The live example below shows exactly this. - 3
Reason 2: the parent rendered. When a component renders, React renders every child it returns, all the way down, whether or not their props changed. React doesn't check props by default, because comparing props costs time too and most components are cheap. This is the biggest source of "extra" renders, and it's the default you opt out of with
memo(Topic 16.3). - 4
Reason 3: a context changed. A component that reads a context with
useContext(SomeContext)oruse(SomeContext)renders again whenever the provider'svaluechanges (compared withObject.is), even if every parent in between skipped rendering. Context updates go straight to the readers. That's why a provider whosevalue={{ user, setUser }}is a new object on every render makes every reader render every time (Topic 9.3). - 5
Props changing is not a reason by itself. Props only change because a parent rendered and passed new ones (reason 2). And a component never re-renders because an ordinary variable, a ref (
ref.current, Phase 8) or a global object changed: React doesn't watch those. If the screen must change, the data has to go through state, props or context. - 6
The cost model. A state update costs: (1) render time: calling every component in the affected subtree, which is plain JavaScript; (2) reconciliation: comparing the new elements with the old ones (Topic 16.2); (3) commit: changing the DOM nodes that differ; then the browser's layout and paint. Steps 3 and later are the expensive ones, and React keeps them as small as possible. So a component that renders often but produces the same output costs some JavaScript time and zero DOM work. Optimise only when steps 1–2 become large: a big subtree, a slow calculation, or thousands of list items.
Explain it without notes
What are the three reasons a React component re-renders?
Why doesn't a re-render always mean a DOM update?
A component reads a context and its parent is wrapped in memo. The context value changes. What happens?
Why does React re-render children even when their props are unchanged?
Practice
A Clock component in the header owns a time state that updates every second, and the header also renders a slow Menu. Restructure it so Menu doesn't re-render every second, without memo.
Predict the logs when a button calls setName("Asha") and name is already "Asha", in a component that has never updated before.
Trade-offs
- ↔
Rendering whole subtrees by default keeps code simple and always correct, at the price of extra JavaScript work that occasionally needs optimising.
- ↔
Moving state down makes renders cheaper but can make state harder to share; lift it up again only as far as it needs to go (Topic 6.3).
Done when you can
I can name the three reasons a component re-renders.
I can explain the difference between rendering and committing to the DOM.
I can predict which components render after a state change.
I know why setting the same state value usually skips rendering.
I can reduce renders by moving state down or passing children.