Why React.memo Keeps Re-Rendering: Reference Identity in Props
See how React.memo compares props, why inline functions, objects and arrays defeat it, how to stabilise references, and why profiling should come before memoizing.
This article is published in English.
React.memo is often treated as a performance switch: wrap a component and re-renders go away. In practice, apps full of memo can still feel sluggish, because slow React UIs usually come from component structure, unstable props, expensive calculations or simply rendering too much, not from a missing wrapper. This guide shows what memo really compares, the prop patterns that silently defeat it, and how to decide whether it is worth using at all.
What memo actually checks
memo does not stop a component from rendering. It compares the new props with the previous ones, prop by prop using Object.is, and skips the render only when all of them are equal. Changes to the component's own state or to a context it reads still cause it to render. Primitive props such as strings, numbers and booleans compare by value, so they work naturally.
import { memo } from "react";
const App = () => {
const name = "John";
const age = 25;
return <UserCard name={name} age={age} />
};
const UserCard = memo(({ name, age }) => {
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
</div>
);
});
If name and age are unchanged, React can reuse the previous output. The trouble starts with functions, objects and arrays, because for those Object.is compares references, not contents.
Inline callbacks are new every render
Here a parent passes an arrow function to a memoized button.
import React, { useState, memo } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<h2>Count: {count}</h2>
<CounterButton
onIncrement={() => setCount((prev) => prev + 1)}
/>
</div>
);
};
const CounterButton = memo(({ onIncrement }) => {
return (
<button onClick={onIncrement}>
Increment
</button>
);
});
Each render of App creates a new function. Its body is identical, but the reference differs, so memo sees a changed prop and renders CounterButton every time.
The fix is to keep the reference stable with useCallback. Because the updater form prev => prev + 1 is used, the dependency array can stay empty.
import React, { useState, useCallback } from "react";
const App = () => {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<h2>Count: {count}</h2>
<CounterButton
onIncrement={handleIncrement}
/>
</div>
);
};
const CounterButton = ({ onIncrement }) => {
return (
<button onClick={onIncrement}>
Increment
</button>
);
};
Notice that in this snippet CounterButton is no longer wrapped in memo. Without the wrapper, the child re-renders with its parent regardless, and the useCallback achieves nothing. The two only pay off together: keep memo(...) around the child from the previous example. And do not add useCallback everywhere; it has its own cost and belongs where it solves a measured problem.
Inline objects and arrays behave the same way
An object literal inside the component body is recreated on every render.
import { memo } from "react";
const App = () => {
const config = {
theme: "dark",
pageSize: 20,
showFilters: true,
};
return (
<div>
<DataGrid config={config} />
</div>
);
};
const DataGrid = memo(({ config }) => {
return (
<div>
<h3>Theme: {config.theme}</h3>
<p>Page Size: {config.pageSize}</p>
</div>
);
});
So is an array of options.
import { memo } from "react";
const App = () => {
const options = [
{ label: "Admin", value: "admin" },
{ label: "Editor", value: "editor" },
{ label: "User", value: "user" },
];
return (
<div>
<Select options={options} />
</div>
);
};
const Select = memo(({ options }) => {
return (
<select>
{options.map((option) => (
<option key={option.value}>
{option.label}
</option>
))}
</select>
);
});
Both produce fresh references each time, so their memoized children render again.
Hoist data that never changes
When the data is static, move it to module scope so it is created once. The options list can live outside the component.
import { useState, useMemo, memo } from "react";
const options = [
{ label: role.toUpperCase(), value: role },
{ label: "Editor", value: "editor" },
{ label: "User", value: "user" },
]
const App = () => {
return (
<div>
<Select options={options} />
</div>
);
};
const Select = memo(({ options }) => {
return (
<select>
{options.map((option) => (
<option key={option.value}>
{option.label}
</option>
))}
</select>
);
});
export default App;
As printed, the first entry refers to a role variable that does not exist at module scope, so this code throws a ReferenceError. Use a literal such as { label: "Admin", value: "admin" }. If an entry genuinely depends on props or state, keep the array inside the component and wrap it in useMemo with those values as dependencies (which is presumably why useMemo is imported).
The configuration object gets the same treatment.
import { memo } from "react";
const config = {
theme: "dark",
pageSize: 20,
showFilters: true,
};
const App = () => {
return (
<div>
<DataGrid config={config} />
</div>
);
};
const DataGrid = memo(({ config }) => {
return (
<div>
<h3>Theme: {config.theme}</h3>
<p>Page Size: {config.pageSize}</p>
</div>
);
});
Measure before you memoize
Useful questions come before any wrapper: which component is actually slow, why it renders, whether that render is expensive, and whether the work can be reduced rather than hidden. The React DevTools Profiler answers most of them, showing which components rendered, why, how long each took and which updates were unnecessary. The checklist on doing less work before memoization expands on that order of operations.
Key takeaways
memohelps only while every prop keeps a stable reference.- Inline functions, objects, arrays and context values routinely defeat it.
useCallbackwithout a memoized child is wasted effort.- Reserve
useMemofor costly calculations or references that must stay stable. - Virtualizing long lists and splitting large components usually gains more than memoizing.
- Profile first; let data decide.