What causes a re-render?
Key idea
The Virtual DOM is the foundation on which React implements a component's lifecycle.
React does not work directly with the real DOM on every change. Instead:
- It builds a virtual tree (Virtual DOM) - JS objects describing the interface structure.
- When state changes (
state,props), React creates a new version of the virtual tree. - It compares it with the old version (the reconciliation process).
- It computes the minimal set of changes that need to be applied to the real DOM.
- It applies them in the commit phase.
And all lifecycle events happen around these actions.
How the Virtual DOM relates to lifecycle phases
Here is the relationship, step by step:
| Stage | What React does with the Virtual DOM | Which methods / hooks are called |
|---|---|---|
| Mounting | React creates the first virtual tree from the JSX components | useEffect (after commit), useLayoutEffect, componentDidMount |
| Updating | React creates a new virtual tree, compares it with the old one -> finds the differences (diffing) | useEffect (cleanup + new), useLayoutEffect, componentDidUpdate |
| Unmounting | React removes the node from the virtual tree -> removes the corresponding DOM elements | cleanup functions in useEffect, componentWillUnmount |
Example: step by step
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('effect');
return () => console.log('cleanup');
});
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}- Mounting
- React calls
Counter()-> creates the Virtual DOM for<button>0</button>. - There is nothing to compare against yet -> creates the DOM element
<button>. - After the commit it calls
useEffect().
render -> diff -> commit -> useEffect- Updating (
setCount)
- React calls
Counter()again -> new Virtual DOM:<button>1</button>. - Compares the old (
<button>0</button>) with the new (<button>1</button>) -> sees a difference in the text. - Changes only the text in the DOM.
- Calls the cleanup of the old effect -> then the new
useEffect.
render -> diff -> minimal DOM update -> cleanup -> new useEffect- Unmounting
- React removes the
CounterVirtual DOM node. - Removes the
<button>element from the DOM. - Calls
cleanup()fromuseEffect.
unmount -> cleanup -> remove DOMRelation to the Render and Commit phases
| Phase | Work with the Virtual DOM | Lifecycle |
|---|---|---|
| Render phase | React re-creates the Virtual DOM (in memory) | the component's render(), reading state, computing JSX |
| Commit phase | React updates the real DOM based on the diff | useLayoutEffect, useEffect, componentDidMount, componentDidUpdate, cleanup |
Key point
The Virtual DOM is a "workspace" where React decides what needs to change. The lifecycle is a "timeline" of when those changes happen and when you can hook into that process.
Visually
(state changed)
↓
┌──────────────┐
│ Render phase │ ← build a new Virtual DOM
└──────────────┘
↓
compare with the previous one (diffing)
↓
┌──────────────┐
│ Commit phase │ ← update the real DOM
└──────────────┘
↓
call useEffect()Would you like me to draw a diagram of a component's lifecycle (mounting -> updating -> unmounting) showing where React works with the Virtual DOM and where the hooks (useEffect, useLayoutEffect, cleanup) are called?
You said:
What causes a re-render?
ChatGPT said:
Great question - understanding exactly what causes a re-render is the key to optimizing React applications. Let's break it all down in detail, without "magic": when React decides to call a component again and why.
What is a "render" in general?
A "render" is a call of the component's function so React understands what JSX (that is, the Virtual DOM) it should display. This does not always mean a change to the real DOM!
React can call a component again, compare the result with the previous Virtual DOM, and... change nothing in the browser if everything is the same.
A re-render happens when React decides that it needs to recompute the JSX.
Here are all the reasons this happens
1. A change to state inside the component
The most obvious one:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}- Every call to
setCount()-> React callsCounter()again. - The component re-renders.
- Then React compares the old and new Virtual DOM (diffing) -> and changes only the changed text in the real DOM.
Even if
setStatesets the same value (setCount(0)whencount === 0), React compares the old and new value and does not trigger a re-render if they are equal (Object.is()comparison).
2. A change to props coming from the parent
If the parent re-rendered and passed new props (by reference or by value), React will re-render the child too.
function Child({ value }) {
console.log('render child');
return <div>{value}</div>;
}
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>+</button>
<Child value={count} /> {/* -> re-renders when count changes */}
</>
);
}Parentchanged -> React callsParent().- The new Virtual DOM includes
<Child value={newCount}>. Child's prop changed -> it re-renders too.
Even if the parent changed an unused piece of state, the component function is still called, but React can optimize this with
React.memo.
3. A change in context (useContext)
If you use context and its value has changed, all the components that read it through useContext() re-render:
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const theme = useContext(ThemeContext); // on change - re-render
return <div>{theme}</div>;
}To keep all context consumers from re-rendering, use selectors or split the contexts.
4. A change in the parent
If the parent re-renders, then React by default re-renders all of its children, because it calls the parent's JSX again:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<Child /> {/* will be called again */}
<button onClick={() => setCount(c => c + 1)}>+</button>
</>
);
}Even if Child does not depend on count, React will still recreate its JSX.
To avoid this, use React.memo(Child).
5. A change to a component's key
If a component's key changes, React treats it as a new element -> unmounts the old one, mounts the new one.
{list.map(item => (
<Row key={item.id} data={item} />
))}If id changes -> Row is created anew (not updated).
6. A change to the parent's Context.Provider, even without a change in value
React calls all consumers again if a new value object is created on every render:
<ThemeContext.Provider value={{ color: 'black' }}>Here a new object is created on every call -> useContext sees a new value -> a re-render.
To avoid this, memoize the value:
<ThemeContext.Provider value={useMemo(() => ({ color: 'black' }), [])}>7. An update through forceUpdate (rare, but possible)
In class components, this.forceUpdate() explicitly forces React to call render() again,
even if state and props have not changed.
Key table
| Reason | What changed | Does it cause a re-render? |
|---|---|---|
setState() inside the component | Yes | Yes |
New props from the parent | Yes | Yes |
| The parent re-rendered | Even without a change in props | Yes |
A context value (useContext) | Yes | Yes |
| A new object/function in props (by reference) | Yes, if the reference changed | Yes |
A new key on the element | Yes | Yes (remounting) |
setState() with the same value | No (React compares with Object.is()) | No |
React.memo() + identical props | No changes | No |
How to avoid unnecessary re-renders
React.memo(Component)- memoization by props.useMemo()- memoization of computations inside the component.useCallback()- memoization of functions passed to children.useContextSelector()or splitting contexts.- Do not create new objects/functions directly in JSX.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.