Fix: React.memo Not Preventing Re-renders
Quick Answer
How to fix React.memo not working — components still re-rendering despite being wrapped in memo, caused by new object/function references, missing useCallback, and incorrect comparison functions.
The Error
You wrap a component in React.memo() to prevent unnecessary re-renders, but the component still re-renders every time the parent renders. There is no error — the optimization simply does not work.
Common symptoms:
- A component wrapped in
React.memore-renders on every parent render. - Adding
React.memohas no measurable performance improvement. - The React DevTools Profiler shows the memoized component highlighted on every render.
- A component re-renders even when its props appear unchanged.
Why This Happens
React.memo performs a shallow comparison of props between renders. It skips re-rendering only if all props pass Object.is() equality. The optimization fails when:
- Props include new object or array literals on every render —
{}and[]create a new reference each time, so shallow comparison always finds them different. - Props include inline functions —
() => {}creates a new function reference on every render. - A parent’s context or state change causes a re-render that produces genuinely new prop values.
- The component uses
useContext— context changes bypassReact.memoentirely. - The comparison function is wrong — a custom comparator passed to
React.memohas a bug. - Children prop is passed — JSX elements (
<div>...</div>) are objects and always create new references.
Fix 1: Memoize Object and Array Props with useMemo
If you pass an object or array as a prop, create it with useMemo so its reference only changes when the data changes:
Broken — new object on every render:
function Parent() {
const [count, setCount] = useState(0);
return (
<MemoizedChild
config={{ theme: "dark", size: "large" }} // New object every render
items={[1, 2, 3]} // New array every render
/>
);
}
const MemoizedChild = React.memo(function Child({ config, items }) {
console.log("Child rendered"); // Runs every time Parent renders
return <div>{config.theme}</div>;
});Fixed — stable references with useMemo:
function Parent() {
const [count, setCount] = useState(0);
const config = useMemo(() => ({ theme: "dark", size: "large" }), []);
const items = useMemo(() => [1, 2, 3], []);
return (
<MemoizedChild config={config} items={items} />
);
}Now config and items have the same reference between renders (as long as their dependencies don’t change), so React.memo’s shallow comparison finds them equal and skips re-rendering.
Why this matters: JavaScript creates a new object or array literal every time a function executes.
{ theme: "dark" }in a render creates a new object on every render — even though the content is identical,Object.is(obj1, obj2)returnsfalsebecause they are different references.React.memocompares references, not deep values.
Fix 2: Memoize Function Props with useCallback
Functions passed as props also create new references on every render:
Broken — inline function as prop:
function Parent() {
const [count, setCount] = useState(0);
return (
<MemoizedButton
onClick={() => console.log("clicked")} // New function every render
/>
);
}Fixed — stable function with useCallback:
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("clicked");
}, []); // Empty deps — function never changes
return <MemoizedButton onClick={handleClick} />;
}When the callback depends on state or props, include them in the dependency array:
const handleSubmit = useCallback(() => {
submitForm(formData); // formData changes when user types
}, [formData]); // Re-create only when formData changesCommon Mistake: Wrapping every function in
useCallback“just in case.”useCallbackitself has overhead — only use it when the function is passed as a prop to a memoized component, used as a dependency in anotheruseEffectoruseMemo, or otherwise needs a stable reference.
Fix 3: React.memo Does Not Block Context Changes
React.memo only checks props. If a component consumes a context with useContext, it re-renders whenever the context value changes — regardless of React.memo:
const ThemeContext = createContext("light");
const MemoizedChild = React.memo(function Child() {
const theme = useContext(ThemeContext); // Context bypasses memo
return <div className={theme}>Content</div>;
});
function Parent() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={theme}>
<MemoizedChild /> {/* Re-renders on every theme change — expected */}
</ThemeContext.Provider>
);
}This is expected behavior. Solutions:
Split context into stable and frequently-changing parts (see Fix: React Context not updating for the splitting pattern).
Move context consumption up and pass the value as a prop — React.memo can then prevent re-renders when the context-derived prop does not change.
Fix 4: Memoize the children Prop
If you pass JSX as children, each render creates new React element objects:
Broken — children bypasses memo:
<MemoizedLayout>
<HeavyComponent /> {/* New React element object every render */}
</MemoizedLayout>Fixed — memoize children explicitly:
const children = useMemo(() => <HeavyComponent />, []);
<MemoizedLayout>{children}</MemoizedLayout>Or restructure so the memoized component does not need to accept children — pass data props instead and let it render its own children.
Fix 5: Use a Custom Comparison Function
For deep comparison of complex props, pass a custom comparator as the second argument to React.memo:
function areEqual(prevProps, nextProps) {
// Return true to skip re-render (props are equal)
// Return false to re-render (props are different)
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.onUpdate === nextProps.onUpdate
);
}
const UserCard = React.memo(function UserCard({ user, onUpdate }) {
return (
<div>
<p>{user.name}</p>
<button onClick={onUpdate}>Update</button>
</div>
);
}, areEqual);Warning: Deep comparison has its own cost. For very large objects, the comparison itself may be slower than just re-rendering. Profile before using custom comparators.
Do not use JSON.stringify for comparison — it is slow, does not handle circular references, and ignores functions:
// Avoid this — slow and incorrect for functions/undefined
const areEqual = (prev, next) =>
JSON.stringify(prev) === JSON.stringify(next);Fix 6: Verify with React DevTools Profiler
Before optimizing, confirm which component is re-rendering and why:
- Install React Developer Tools.
- Open DevTools → Profiler tab.
- Click Record, interact with your app, click Stop.
- Click on any component bar in the flame chart.
- The panel shows why the component rendered: “Props changed”, “Context changed”, “Hooks changed”, or “Parent rendered”.
If the Profiler shows “Props changed”, click into it to see which specific prop changed. This points directly to which prop needs memoization.
Enable “Highlight updates” in React DevTools:
Settings → General → “Highlight updates when components render.” Components flash blue when they re-render — a component that flashes constantly despite being memoized has a reference stability problem.
Fix 7: When NOT to Use React.memo
React.memo adds complexity and has overhead. Skip it when:
- The component renders quickly — memoization overhead may exceed the rendering cost.
- Props always change — if every render produces genuinely new prop values (e.g., a timer updating every second), memoization never helps.
- The component rarely re-renders anyway — only leaf components that render frequently are worth memoizing.
- The component is small — a simple
<Button>with minimal DOM output costs almost nothing to re-render.
Focus optimization effort on:
- Components that render large lists (use
React.memo+useCallbackfor list item components). - Components with expensive computations (use
useMemofor the computation itself). - Components deep in the tree that are re-rendering due to unrelated parent state changes.
Real-world scenario: A dashboard with 50+ chart components all re-rendering when a single counter updates is a good candidate for
React.memo. A simple navigation bar that re-renders when page state changes is probably not worth memoizing — it renders in microseconds regardless.
Still Not Working?
Check if the component is defined inside another component. A component defined inside a render function gets a new reference on every render, breaking memoization:
// Broken — new component type every render
function Parent() {
const Child = React.memo(() => <div>Child</div>); // New type each render!
return <Child />;
}
// Fixed — define outside
const Child = React.memo(() => <div>Child</div>);
function Parent() {
return <Child />;
}Check for key prop changes. If the key prop on a memoized component changes, React unmounts and remounts the component entirely — React.memo is irrelevant. A changing key is intentional when you want to reset component state.
Check React.StrictMode double renders. In development with <React.StrictMode>, React intentionally renders components twice to detect side effects. This double render appears in DevTools but does not happen in production. Memoization works correctly in production even if DevTools shows extra renders in development.
For related performance issues, see Fix: React Too Many Re-renders and Fix: React useEffect Infinite Loop.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React.lazy and Suspense Errors (Element Type Invalid, Loading Chunk Failed)
How to fix React.lazy and Suspense errors — Element type is invalid, A React component suspended while rendering, Loading chunk failed, and lazy import mistakes with named vs default exports.
Fix: React Query (TanStack Query) Infinite Refetching Loop
How to fix React Query refetching infinitely — why useQuery keeps fetching, how object and array dependencies cause loops, how to stabilize queryKey, and configure refetch behavior correctly.
Fix: Vite Build Chunk Size Warning (Some Chunks Are Larger Than 500 kB)
How to fix Vite's chunk size warning — why bundles exceed 500 kB, how to split code with dynamic imports and manualChunks, configure the chunk size limit, and optimize your Vite production build.
Fix: Webpack HMR (Hot Module Replacement) Not Working
How to fix Webpack Hot Module Replacement not updating the browser — HMR connection lost, full page reloads instead of hot updates, and HMR breaking in Docker or behind a proxy.