Posted on: 04/09/2026(updated)
By default, when a parent component updates, all of its children re-render recursively—regardless of whether their props changed.
Memoization provides an opt-in mechanism to tell React to skip re-rendering a component if its inputs haven't changed, and reuse the last output.
React.memo wraps a component to perform a shallow comparison (Object.is) of previous and current props before rendering.
// HeavyChild is wrapped in React.memo
const HeavyChild = React.memo(({ config, onClick }) => {
return <button onClick={onClick}>{config.title}</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
// Even though the contents are identical, these receive NEW memory
// addresses on every parent re-render, breaking React.memo instantly.
const config = { title: "Dashboard" };
const handleClick = () => console.log("Clicked");
return (
<div>
<button onClick={() => setCount(count + 1)}>Counter: {count}</button>
<HeavyChild config={config} onClick={handleClick} />
</div>
);
};
React.memo will fail to prevent re-renders in the code above. Even though config and handleClick look identical on every render. JavaScript creates brand-new memory references for them every single time Parent re-renders (when count changes).
Here is how React.memo fails:
Parent.To make React.memo actually work here, we have two options:
If config and handleClick don't depend on state or props inside Parent, move them outside the render function entirely so their references never change:
// Defined once when the module loads—memory reference never changes
const CONFIG = { title: "Dashboard" };
const HANDLE_CLICK = () => console.log("Clicked");
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Counter: {count}</button>
<HeavyChild config={CONFIG} onClick={HANDLE_CLICK} />
</div>
);
};
If the object or function relies on values inside Parent, wrap them in hooks:
const Parent = () => {
const [count, setCount] = useState(0);
// Preserves the same object reference across renders
const config = useMemo(() => ({ title: "Dashboard" }), []);
// Preserves the same function reference across renders
const handleClick = useCallback(() => console.log("Clicked"), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Counter: {count}</button>
<HeavyChild config={config} onClick={handleClick} />
</div>
);
};
To stop parent updates from generating new memory references and ruining React.memo, you must explicitly stabilize non-primitive props.
const HeavyChild = React.memo(({ config, onClick }) => {
return <button onClick={onClick}>{config.title}</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
// 1. Stable object reference across renders
const config = useMemo(() => ({ title: "Dashboard" }), []);
// 2. Stable function reference across renders
const handleClick = useCallback(() => {
console.log("Clicked");
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Counter: {count}</button>
{/* HeavyChild successfully SKIPS re-renders now */}
<HeavyChild config={config} onClick={handleClick} />
</div>
);
};
When stabilizing references in the parent component is impractical (or when dealing with deeply nested object props), React.memo accepts a custom comparator function as its second argument.
// Compare specific nested values directly instead of memory references
const HeavyChild = React.memo(
({ user }) => <div>{user.profile.details.name}</div>,
(prevProps, nextProps) => prevProps.user.id === nextProps.user.id
);
Memoization is not a free performance boost; every hook and HOC adds runtime overhead:
children prop) without hooks.| Tool | What It Caches | When to Use It |
|---|---|---|
| React.memo | Rendered DOM / Component Output | Heavy components rendered frequently with identical props. |
| useCallback | Function Instance | Passing callbacks into React.memo wrapped child components. |
| useMemo | Value / Object Reference | Preserving object references for React.memo OR caching expensive calculations. |