10LOC
Reactintermediate

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

  • isEqual is the second argument to memo. React calls it with the previous and next props on every re-render attempt; returning true means "props are equivalent, skip the render."
  • The comparison is scoped to the fields that actually affect the output: id, name, and the contents of tags, compared element-by-element rather than by array reference.
  • UserCardBase is the plain, unmemoized component; UserCard is 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: arePropsEqual returning true means equal, don't re-render — the opposite of a typical sort-style comparator.
  • The comparator only runs on re-render attempts triggered by the parent; it doesn't affect UserCard's own useState/useReducer updates.
  • tags.every here 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; memo memoizes 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.