Sometimes the data stays the same, but its reference changes.
Consider these two objects:
const previousUser = { name: "Alex" };
const nextUser = { name: "Alex" };
previousUser === nextUser; // false
Object.is(previousUser, nextUser); // falseThey contain the same information, but they are separate objects.
By default, React.memo compares each prop with its previous value using Object.is. It doesn’t inspect an object’s nested properties to determine whether its contents are identical. React.memo reference
Here’s how that affects a component:
import { memo, useState } from "react";
const UserCard = memo(function UserCard({ user, options }) {
console.log("UserCard rendered");
return (
<article>
<h2>{user.name}</h2>
<p>{user.role}</p>
{options.showEmail && <p>{user.email}</p>}
</article>
);
});
export default function App() {
const [count, setCount] = useState(0);
const user = {
name: "Alex",
role: "Frontend Engineer",
email: "alex@example.com",
};
return (
<>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<UserCard user={user} options={{ showEmail: true }} />
</>
);
}Clicking the button updates count, so App renders again. Each execution creates new user and options objects.
The state update triggers the parent render. The new references prevent the default memo comparison from skipping the child’s render. Minimizing prop changes
Two details matter when trying this example:
It assumes React Compiler is disabled. With the compiler enabled, equivalent optimizations may happen automatically. React Compiler introduction
In development, StrictMode deliberately calls component bodies an extra time to detect impure rendering. You may see duplicate console logs; that doesn’t mean the DOM was updated twice. StrictMode documentation
Start with the simplest solution
If the data is static, define it outside the component:
import { memo, useState } from "react";
const USER = {
name: "Alex",
role: "Frontend Engineer",
email: "alex@example.com",
};
const OPTIONS = { showEmail: true };
const UserCard = memo(function UserCard({ user, options }) {
return (
<article>
<h2>{user.name}</h2>
<p>{user.role}</p>
{options.showEmail && <p>{user.email}</p>}
</article>
);
});
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<UserCard user={USER} options={OPTIONS} />
</>
);
}Now rendering App doesn’t recreate those objects.
Use useMemo when the value depends on changing inputs
When an object is derived from props or state, useMemo can reuse the calculated value while its dependencies remain unchanged:
import { memo, useMemo } from "react";
const UserCard = memo(function UserCard({ user }) {
return (
<article>
<h2>{user.name}</h2>
<p>{user.role}</p>
<p>{user.email}</p>
</article>
);
});
export default function Profile({ name, role, email }) {
const user = useMemo(
() => ({ name, role, email }),
[name, role, email]
);
return <UserCard user={user} />;
}This is useful when preserving the reference enables a worthwhile optimization. Include every reactive value used in the calculation; omitting dependencies can leave you with stale data. useMemo reference
Sometimes you can remove the intermediate object entirely:
import { memo } from "react";
const UserCard = memo(function UserCard({
name,
role,
email,
showEmail,
}) {
return (
<article>
<h2>{name}</h2>
<p>{role}</p>
{showEmail && <p>{email}</p>}
</article>
);
});
export default function Profile({ name, role, email }) {
return (
<UserCard
name={name}
role={role}
email={email}
showEmail
/>
);
}For these string and boolean props, unchanged values compare equal without memoizing an object. Simplifying props
Functions can also have changing references
An inline callback creates a new function whenever the parent executes:
<SaveButton onSave={() => saveUser(userId)} />If the child is memoized and profiling shows that skipping its renders helps, useCallback can preserve the callback reference:
import { memo, useCallback } from "react";
const SaveButton = memo(function SaveButton({ onSave }) {
return <button onClick={onSave}>Save user</button>;
});
export default function UserActions({ userId, saveUser }) {
const handleSave = useCallback(
() => saveUser(userId),
[saveUser, userId]
);
return <SaveButton onSave={handleSave} />;
}Here, saveUser is explicitly a prop, so it belongs in the dependency array. If it changes on every parent render, handleSave will change too.
If saveUser were instead a function imported at module scope, it wouldn’t be a reactive dependency.
useCallback caches the function itself, not the result of calling it. useCallback reference
A re-render isn’t automatically a performance problem
Rendering means React calls your component to calculate its output. React then determines which DOM changes, if any, are needed. An unchanged result may require no DOM update. Render and Commit
Also, memo is a performance optimization, not a guarantee that React will always skip rendering. Changes to the component’s own state or a context it consumes can still cause re-renders. React.memo documentation
Before adding more memoization, ask:
Which interaction is actually slow?
Is the expensive work rendering or a calculation?
Is an unstable reference defeating an existing optimization?
Could simpler props or more local state help?
Does profiling confirm an improvement?
Adding useMemo and useCallback everywhere can make code harder to maintain without improving the experience. When useCallback is useful
Finally, new references are often necessary. When an object in state changes, create an updated object and set the new state instead of mutating the existing object. Updating Objects in State
The goal is to understand where identity matters and whether stabilizing it solves a measured problem.
Before optimizing, ask: “What triggered this render, and is a new reference preventing the optimization I expected?”
