To optimize React, we first need to understand how React behaves out of the box.
The most important rule of React rendering is simple and aggressive:
When a Parent Component renders, all of its Child Components render recursively.
It does not matter if the props passed to the child have changed. It does not matter if the child is completely static. If the parent runs, the child runs.
function Parent() {
const [count, setCount] = useState(0); // Parent state changes
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
{/* This Child will re-render every time the button is clicked,
even though it has no props! */}
<BigList />
</div>
);
}This behavior is intentional (it ensures the UI is always up to date), but it can cause performance issues if the Child is expensive (e.g., a large list or a complex graph).
To stop this chain reaction, we need a gatekeeper. We use React.memo.
React.memo is a Higher Order Component that wraps our child component. It changes the default behavior to:
Only re-render this component if the props have changed.
const BigList = React.memo(function BigList() {
console.log("BigList Rendered");
return <ul>...</ul>;
});Now, if the Parent renders but the props for BigList are identical, React skips rendering BigList.
One might think React.memo solves everything. It doesn't. We will often find that even wrapped in React.memo, the child still re-renders.
Why ? Because of how JavaScript compares data.
When React.memo checks Did props change ?, it uses a shallow comparison (===).
true === true, 1 === 1. These are easy.React.memo and causing unnecessary re-renders.To make React.memo work, we need to ensure that objects, arrays and functions keep the same memory address across renders.
This hook freezes a function definition. It ensures the pointer (memory address) stays the same unless dependencies change.
// WITHOUT useCallback: New address every render
const handleClick = () => console.log("Clicked");
// WITH useCallback: Same address forever (because dependency array is empty)
const handleClick = useCallback(() => {
console.log("Clicked");
}, []);This hook caches the result of a calculation or the creation of an object.
// WITHOUT useMemo: New object created every render
const boxStyle = { margin: 10, color: "red" };
// WITH useMemo: The object is created once and cached
const boxStyle = useMemo(() => {
return { margin: 10, color: "red" };
}, []);Note: useMemo is also used for expensive math calculations (filtering 10k items), not just stabilizing objects.
To truly stop a re-render chain, we usually need all three parts working together. If we miss one, the optimization breaks.
| Component | Role | Code |
|---|---|---|
| Child | The Guard (Checks Props) | export default React.memo(Child) |
| Parent | Stabilize Functions | useCallback(() => {...}, []) |
| Parent | Stabilize Objects | useMemo(() => ({...}), []) |
Do not apply this structure to every component.
useMemo checking dependencies is often higher than just re-rendering the HTML.useCallback and useMemo make code harder to read. Use them only when the performance gain justifies the complexity.