React.memo with a custom comparator for deep-equal props
Published September 22, 2026
import { memo } from "react";
type Props = { user: { id: string; name: string; tags: string[] } };
const isEqual = (prev: Props, next: Props) =>
prev.user.id === next.user.id &&
prev.user.name === next.user.name &&
prev.user.tags.length === next.user.tags.length &&
prev.user.tags.every((tag, i) => tag === next.user.tags[i]);
const UserCardBase = ({ user }: Props) => (
<div className="card">
<h3>{user.name}</h3>
<ul>{user.tags.map((tag) => <li key={tag}>{tag}</li>)}</ul>
</div>
);
export const UserCard = memo(UserCardBase, isEqual);What
memo(Component, arePropsEqual) skips a re-render when its custom comparator says the new props are equivalent to the old ones — even if they're different object references.
Why it matters
memo without a second argument uses Object.is on every prop, which compares objects by reference. If a parent creates a new user object every render — spreading it, mapping over an array, whatever — plain memo does nothing: the reference changed, so it re-renders regardless of whether the actual data is the same. Deep-cloning or deep-comparing on every render defeats the purpose of memoizing in the first place. A custom comparator lets you write exactly the equality check that matches your data shape, once, instead of reaching for a generic (and slower) deep-equal utility.
How it works
isEqualis the second argument tomemo. React calls it with the previous and next props on every re-render attempt; returningtruemeans "props are equivalent, skip the render."- The comparison is scoped to the fields that actually affect the output:
id,name, and the contents oftags, compared element-by-element rather than by array reference. UserCardBaseis the plain, unmemoized component;UserCardis the exported, memoized version. Keeping them separate makes it obvious which one carries the comparator.
Gotchas
- Get the return value backwards and you'll silently break re-renders:
arePropsEqualreturningtruemeans equal, don't re-render — the opposite of a typicalsort-style comparator. - The comparator only runs on re-render attempts triggered by the parent; it doesn't affect
UserCard's ownuseState/useReducerupdates. tags.everyhere assumes order matters (["a", "b"]isn't equal to["b", "a"]). If your data doesn't guarantee order, sort before comparing or compare as sets.
Related
useMemo— memoizes a value between renders in the same component;memomemoizes a component's output across renders triggered by its parent.- Structural-sharing state updates (e.g., only replacing the one array element that changed) — often removes the need for a custom comparator entirely, by keeping references stable at the source instead of comparing them downstream.