React, the free, open-source JavaScript library for web and native user interfaces (UI), enables developers to seamlessly build user interfaces through the use of individual pieces called components. React is optimized to handle re-renders efficiently, but as an application scales, unnecessary component updates can become a real performance bottleneck. That’s why it’s crucial to understand exactly when a React component re-renders, and to separate common myths from actual mechanics. Without this understanding, even well-structured components can become sources of subtle bugs or sluggish performance. By gaining clarity on re-render behavior, developers can write more predictable, efficient code and build UIs that stay responsive as an app grows.
When does a React component re-render?
When I ask React developers this question, the most common answers are:
- The Component’s state changes ✅
- The Context the component subscribes to changes ✅
- The Component’s props change ❌
That third point – “props change” – is a widespread misconception. Let’s dig into an example:
export const ParentComponent = () => {
let count = 0;
console.log(‘ParentComponent renders’);
return (
<div>
<button onClick={() => {
console.log(‘count’, count);
count += 1;
}}>
Increment {count}
</button>
<ChildComponent count={count} />
</div>
);
};
const ChildComponent = ({ count }) => {
console.log(‘ChildComponent renders’);
return (
<div>
<p>{count}</p>
</div>
);
};
At first glance, it might seem like the ChildComponent is receiving new props (count) each time the button is clicked. However, you’ll notice in the console that neither ParentComponent nor ChildComponent re-render – even though the count value is changing.
Why? Because count is just a regular local variable, not part of the React state. Updating it doesn’t trigger a re-render, and therefore React doesn’t bother checking or propagating any “prop changes.” The component tree stays static.
The real reason components re-render
- The Component’s state changes ✅
- The Context the component subscribes to changes ✅
- Parent Component re-renders ✅
The main rule to remember when it comes to re-renders is that every Component re-render starts with a state change.
Moving state down
Let’s take a look at those two simple components:
const ParentComponent = (props) => {
const [count, setCount] = useState(0)
console.log(‘ParentComponent component renders’)
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment {count}</button>
<ChildComponent />
</div>
)
}
const ChildComponent = () => {
console.log(‘ChildComponent component renders’)
return (
<div>
<p>Dummy component</p>
</div>
)
}
Whenever we click the Increment button, both ParentComponent and ChildComponent re-render – even though the child component has no relation to the state (count) or the button logic. This happens because the state is managed in the parent, and any state change causes the entire component (and its subtree) to re-evaluate.
A better way: move the state closer to where it’s used
There’s a simple and powerful solution: move the state down the component tree, closer to where it’s actually needed.
export const ParentComponent = (props) => {
console.log(‘ParentComponent component renders’)
return (
<div>
<Counter />
<ChildComponent />
</div>
)
}
const ChildComponent = () => {
console.log(‘ChildComponent component renders’)
return (
<div>
<p>Dummy component</p>
</div>
)
}
const Counter = (props) => {
const [count, setCount] = useState(0)
console.log(‘Counter component renders’)
return (
<button onClick={() => setCount(count + 1)}>Increment {count}</button>
)
}
With this version, clicking the Increment button only triggers a re-render of the Counter component. The ParentComponent and ChildComponent remain completely untouched and unaffected.
Memoization
Sometimes, isolating the state or restructuring components isn’t feasible – especially in complex UIs where components are tightly coupled. In such cases, we can turn to memoization to fight unnecessary re-renders.
One of the most effective tools for this in React is the React.memo higher-order component. It prevents a component from re-rendering – unless its props actually change – providing a shallow comparison by default.
Let’s take a look at an example.
export const ParentComponent = () => {
const [count, setCount] = useState(0);
const user = { name: ‘Alice’, email: ‘alice @example.com’ };
console.log(‘ParentComponent renders’);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment {count}</button>
<ChildComponent user={user} />
</div>
);
};
const ChildComponent = ({ user }) => {
console.log(‘ChildComponent renders’);
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
};
In this version, every time we click the increment button, both ParentComponent and ChildComponent re-render – even though the user object hasn’t changed in content. That’s because the user object is recreated on every render, so React sees it as a “new” prop.
Using React.memo and useMemo together
Now, let’s apply memoization to optimize this.
const ChildComponent = React.memo(({ user }) => {
console.log(‘ChildComponent renders’);
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
Then, in the parent:
export const ParentComponent = () => {
const [count, setCount] = useState(0);
const user = React.useMemo(() => ({
name: ‘Alice’,
email: ‘alice @example.com’
}), []);
console.log(‘ParentComponent renders’);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment {count}</button>
<ChildComponent user={user} />
</div>
);
};
By wrapping ChildComponent with React.memo and memoizing the user object with useMemo, React can now detect that the props haven’t changed – so it skips re-rendering the child even when the parent updates.
✅ Important caveat: For React.memo to be effective, all props passed to the memoized component must be stable across renders. If you accidentally pass a newly created object, array, or function as a prop – even if the values are the same – React will treat it as changed.
Here’s an example that won’t work as intended:
export const ParentComponent = () => {
const [count, setCount] = useState(0);
// ❌ New object created on every render
const user = { name: ‘Alice’, email: ‘alice @example.com’ };
console.log(‘ParentComponent renders’);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment {count}</button>
<ChildComponent user={user} />
</div>
);
};
Children pattern
To improve an app’s performance, you can take advantage of how React’s diffing and reconciliation algorithm works. One useful optimization is to extract child components out of the parent’s render flow, so that they aren’t recreated or re-evaluated on every state change. This can be achieved by passing components off as externally defined props – most commonly using the children prop (or any other custom prop).
This works particularly well when child components do not depend on the parent’s state or props. Since they are declared outside of the parent component and passed in by reference, React can treat them as the same elements between renders and avoid re-rendering them unnecessarily.
Here’s how this pattern could look in practice:
const App = (props) => {
return (
<List>
<ListItem />
<ListItem />
<ListItem />
</List>
);
}
const List = ({ children }) => {
const [counter, setCounter] = useState(0);
console.log(“List rendered”);
return (
<div>
<h2>Counter: {counter}</h2>
<button onClick={() => setCounter(counter + 1)}>Increment</button>
{children}
</div>
);
};
const ListItem = (props) => {
console.log(“ListItem rendered”);
return <p>dummy message</p>;
};
If you run this code, you’ll see that incrementing the counter causes the List component to re-render, but does not trigger a re-render of the ListItem components. That’s because those children were declared statically in App, outside of List, and React preserves their identities between renders.
✅ Important caveat: this optimization works only when the child components do not rely on values from the parent’s scope (e.g., props derived from parent state). If you pass dynamic props to the children, they will still re-render when those props change – regardless of how the component is passed.
This is a simple and powerful example of component composition and how understanding React’s reconciliation model can lead to subtle but effective performance improvements.
Lists and key keyword
It is often said that providing the essential keyword whenever using a loop to render a list of components is crucial for performance and helps avoid unnecessary re-renders. This is another big myth. What’s important to know about lists is that they are only there to help React understand the identity of each component between renders. It allows React to correctly match up elements in the virtual document object module (DOM) with their counterparts in the actual DOM when diffing changes.
This identity mapping is essential for maintaining a proper state and behavior of components – especially when list items are added, removed, or reordered. Without a stable and unique key, React may reuse a component instance incorrectly, leading to subtle bugs or UI inconsistencies.
However, while it helps developers avoid weird bugs when rendering dynamic lists, the key itself does not prevent re-renders. If a parent component re-renders and the props of a list item change (or even if they don’t, but the parent re-renders anyway), the child components will still re-render unless they are memoized (e.g., via React.memo).
In other words, the key helps with reconciliation (i.e., deciding which DOM elements to update, create, or remove) – but not with render skipping. Avoiding re-renders requires explicit memoization strategies and thoughtful prop/state management.
Summary
Understanding why and when components re-render is the first step toward building truly efficient UIs. With this knowledge, you’ll be better equipped to recognize unnecessary re-renders, isolate state appropriately and apply optimizations where they matter most.
This is just the tip of the iceberg. React’s rendering model is a deep and fascinating topic – I hope this gave you the spark to explore it further and start applying these patterns in your own projects with confidence
Is your company facing performance issues with a laggy UI? Or are you looking for a reliable technical partner to build and maintain high-performing user interfaces? Get in touch with us through this form – we’d be happy to help.
About the authorBartłomiej Wach
Senior Frontend Engineer
A Frontend Developer with 8 years of experience, Bartłomiej specializes in building modern, scalable applications with React. Passionate about JavaScript, application performance and thoughtful UI&UX design, he has developed applications for a range of industries – especially investment banking. He enjoys mentoring, sharing knowledge and contributing to a culture of continuous learning and technical excellence.