Why Components Re-Render
A re-render is React calling your component function again to produce a fresh description of the UI. It is not the same thing as React touching the DOM — most re-renders are cheap and never reach the browser. Knowing exactly what triggers a re-render, and which triggers actually cost you, is the difference between guessing at performance and fixing it with intent.
What “re-render” actually means
When a component re-renders, React runs its function body again, computes new JSX, and compares the result to the previous render in memory (the “reconciliation” step). Only the parts of the DOM that genuinely changed get committed to the browser. So the work of a re-render is: running your component (cheap-to-expensive depending on what it does) plus diffing its output (usually cheap).
This distinction matters because developers often blame “too many renders” when the real cost is what each render does — heavy computation, large subtrees, or layout thrashing during commit.
The three triggers
There are exactly three reasons a component re-renders.
1. Its own state changed
Calling a state setter schedules a re-render of that component, as long as the new value differs (by Object.is) from the current one.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Clicked {count} times
</button>
);
}
Setting state to the same value bails out — React skips the render entirely.
const [name, setName] = useState("Ada");
// This does NOT trigger a re-render: same value, Object.is equal.
setName("Ada");
2. Its parent re-rendered
When a component renders, all of its children re-render by default — regardless of whether their props changed. This is the cascade that causes most surprises.
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
{/* Child re-renders on every click, even though it has no props */}
<Child />
</div>
);
}
function Child() {
console.log("Child rendered");
return <p>I have nothing to do with count.</p>;
}
Output:
Child rendered
Child rendered // logged again on each parent click
This is fine when Child is cheap. It becomes a problem when Child is expensive or sits at the top of a large subtree.
3. A context it consumes changed
Any component calling useContext re-renders when that context’s value changes, no matter how deep it sits.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function ThemedLabel() {
const theme = useContext(ThemeContext);
return <span className={theme}>Current theme: {theme}</span>;
}
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
Toggle
</button>
<ThemedLabel />
</ThemeContext.Provider>
);
}
A common gotcha: passing a fresh object literal as value (e.g. value={{ theme, setTheme }}) re-renders every consumer on every provider render, because the object identity changes each time.
Triggers compared
| Trigger | Scope | Bails out when… | Typical fix |
|---|---|---|---|
| State change | The component itself | Next value is Object.is-equal | Avoid redundant setState |
| Parent render | All descendants | Wrapped in React.memo with equal props | Memoize, or restructure tree |
| Context change | All consumers | Context value identity is stable | Split contexts, memoize value |
When re-renders actually cost you
Re-rendering is usually cheap. It starts hurting when:
- A render runs an expensive calculation on every pass (sorting, filtering big lists, parsing).
- A small state change at the top of the tree cascades into hundreds of child renders.
- Renders cause synchronous layout reads/writes during commit.
Measure before you optimize. Wrapping everything in
React.memoanduseMemoadds its own comparison cost and complexity. Most apps need it in a handful of hot spots, not everywhere.
Spotting wasted renders
The React DevTools Profiler is the authoritative way to see what re-renders and why.
- Open React DevTools → Profiler, click record, interact with your app, then stop.
- The flamegraph shows each commit; grey bars are components that did not re-render.
- Enable “Record why each component rendered” in the Profiler settings to see whether a render came from props, state, hooks, or a parent.
For a quick live view, turn on “Highlight updates when components render” in the DevTools Components-tab settings. Re-rendering components flash a colored border — if a region flashes on an unrelated interaction, you have found a wasted render.
You can also instrument programmatically with the <Profiler> component:
import { Profiler } from "react";
function onRender(id, phase, actualDuration) {
console.log(`${id} (${phase}) took ${actualDuration.toFixed(2)}ms`);
}
<Profiler id="Sidebar" onRender={onRender}>
<Sidebar />
</Profiler>;
Output:
Sidebar (update) took 0.43ms
Sidebar (update) took 0.39ms
Sub-millisecond updates are nothing to worry about — that number tells you the render is cheap and not worth optimizing.
Best practices
- Treat re-renders as normal; only chase the ones the Profiler proves are expensive.
- Push state down to the smallest component that needs it so changes don’t cascade from the top.
- Lift expensive children out as
childrenprops so they aren’t re-created when the parent’s state changes. - Reach for
React.memoanduseMemo/useCallbackat measured hot spots, not preemptively everywhere. - Keep context
valueidentities stable and split unrelated state into separate contexts. - Use “Highlight updates” for a fast gut-check, and the Profiler with “why did this render” for the real diagnosis.