Skip to content
React rc performance 4 min read

React.memo

By default a React component re-renders whenever its parent re-renders, even if its props are identical to last time. Most of the time that is harmless—React is fast and the render does no real work. But when a child is genuinely expensive and its parent updates often, those wasted renders add up. React.memo is the tool for exactly this case: it wraps a component so React skips the render when its props have not changed.

What React.memo does

React.memo is a higher-order component. You wrap a function component with it and get back a memoized version. Before re-rendering, React compares the new props against the previous props using a shallow equality check. If every prop is the same reference (or primitive value), React reuses the previous result and skips the render entirely.

import { memo } from "react";

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting rendered");
  return <h1>Hello, {name}</h1>;
});

Now Greeting only re-renders when name actually changes, regardless of how many times its parent renders.

import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
      <Greeting name="Ada" />
    </>
  );
}

Output:

Greeting rendered      // initial mount only
// clicking the button updates count but logs nothing more

Without memo, clicking the button would log "Greeting rendered" on every click, because App re-renders and pulls its children along.

How shallow comparison works

Shallow comparison checks each prop with Object.is. For primitives—strings, numbers, booleans—this compares by value, so "Ada" === "Ada" is equal. For objects, arrays, and functions, it compares by reference, not by contents. Two objects with identical fields are not shallowly equal if they were created separately.

Prop typeCompared by{a:1} vs {a:1}
Primitive (string, number, boolean)ValueEqual
Object / arrayReferenceNot equal
FunctionReferenceNot equal

This single fact explains nearly every case where React.memo “doesn’t work.”

Why memo silently fails

The most common mistake is passing a freshly created object or function as a prop. Each parent render builds a brand-new value, so the shallow check always sees a difference and the memo is bypassed.

const Profile = memo(function Profile({ user, onEdit }) {
  console.log("Profile rendered");
  return <button onClick={onEdit}>{user.name}</button>;
});

function Page() {
  const [count, setCount] = useState(0);
  // ❌ New object and new function on every render
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <Profile user={{ name: "Ada" }} onEdit={() => console.log("edit")} />
    </>
  );
}

Output:

Profile rendered      // logs on EVERY count click — memo defeated

Here memo is doing its job perfectly; the props really are different objects each time. The fix is to give those props stable identities.

Pairing with useMemo and useCallback

To keep object and function props stable across renders, memoize them in the parent with useMemo (for values) and useCallback (for functions). Now their references survive re-renders, the shallow check passes, and the memoized child stays put.

import { memo, useCallback, useMemo, useState } from "react";

const Profile = memo(function Profile({ user, onEdit }) {
  console.log("Profile rendered");
  return <button onClick={onEdit}>{user.name}</button>;
});

function Page() {
  const [count, setCount] = useState(0);

  // ✅ Stable references across renders
  const user = useMemo(() => ({ name: "Ada" }), []);
  const handleEdit = useCallback(() => console.log("edit"), []);

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <Profile user={user} onEdit={handleEdit} />
    </>
  );
}

Output:

Profile rendered      // mount only — clicks no longer re-render Profile

Tip: React.memo, useMemo, and useCallback are a team. Memoizing a child without stabilizing its object and function props accomplishes nothing—the comparison fails every time and you pay the cost of the check for no benefit.

Custom comparison functions

React.memo accepts an optional second argument: a comparison function (prevProps, nextProps). Return true to skip the render (props considered equal) or false to re-render. Use it when shallow comparison is too coarse—for example, when you only care about one field of a larger object.

const Row = memo(
  function Row({ item }) {
    return <li>{item.label}</li>;
  },
  (prev, next) => prev.item.id === next.item.id && prev.item.label === next.item.label
);

Be careful: the comparison runs on every potential render, so a deep or expensive comparator can cost more than the render it prevents. Note also that the function’s return value is the opposite of the shouldComponentUpdate convention from class components—true means “equal, do not update.”

When to use it (and when not to)

React.memo helps when all of these hold: the component is measurably expensive to render, its parent re-renders frequently, and its props are stable or easy to stabilize. It is the function-component equivalent of PureComponent.

It is not a default you sprinkle everywhere. Wrapping a cheap component adds a comparison on every render and clutters the code for no measurable gain. If a component renders in well under a millisecond, skip the memo and spend the effort elsewhere.

Best practices

  • Profile first with React DevTools; only memoize components the data shows are hot.
  • Stabilize object and function props with useMemo/useCallback, or memo will silently do nothing.
  • Prefer passing primitives or stable references over inline {} and () => {} literals to memoized children.
  • Reach for a custom comparator only when shallow equality is genuinely insufficient—and keep it cheap.
  • Remember that children passed as the children prop are objects too; a new JSX tree each render breaks memoization.
  • Re-measure after wrapping a component to confirm the wasted renders actually disappeared.
Last updated June 14, 2026
Was this helpful?