Skip to content
ExponaBLOG
REACT & NEXT.JS · DAY 009 / 100

When React.memo is worth it: what developers should know

Skip re-renders only where they actually hurt.

Expona Admin7 min read

React.memo is one of those optimization tools that look like a silver bullet until you actually measure and find out it does almost nothing in your codebase. When React.memo is worth it boils down to one principle: you should only memoize components where props stay stable and rendering is genuinely expensive. Let me show you how to identify those moments and avoid wasting time on premature optimization.

Measure with the React DevTools Profiler first

Before writing a single React.memo line, open the React DevTools Profiler. I can't stress this enough because I've spent hours optimizing components that took 0.2ms to render. In production, that's noise.

Here's my workflow. I'll instrument a page, record a session while performing the actual user action that feels slow, then look at the flame chart. The Profiler shows you exactly which components render, how long they take, and why they rendered. You're looking for components that:

  • Take more than 5ms to render
  • Render repeatedly when they shouldn't
  • Are inside a hot loop (like a list of 1000 items)

In a real example, I was debugging a CRM dashboard where a stats card component felt sluggish. I profiled it, saw it was re-rendering 40 times per second when the parent context updated, but the actual render time was 1.2ms. The card wasn't the problem—it was how often it was being called. React.memo wouldn't have helped there. I needed to split the context or use selector hooks.

The key insight: memoization only helps if the component is expensive to render AND props are stable. If either is false, you're adding complexity for zero gain.

Useful for heavy rows in long lists

This is where React.memo actually earns its place. Long virtualized lists are the classic win case. Imagine a table with 10,000 rows where each row has conditional rendering, computed styles, and icon components. Without memoization, scrolling becomes jank because the entire table re-renders on every scroll event.

Here's a realistic example:

tsx
// ❌ Without memoization - every scroll re-renders all visible rows
const OrderRow = ({ order, onSelect, isHighlighted }) => {
  const statusColor = getStatusColor(order.status); // Computed every time
  const formattedDate = formatDate(order.createdAt); // Computed every time
  
  return (
    <tr className={isHighlighted ? 'highlighted' : ''}>
      <td>{order.id}</td>
      <td style={{ color: statusColor }}>{order.status}</td>
      <td>{formattedDate}</td>
      <td>
        <button onClick={() => onSelect(order.id)}>View</button>
      </td>
    </tr>
  );
};

// ✅ With memoization (if props are stable)
const OrderRow = React.memo(({ order, onSelect, isHighlighted }) => {
  const statusColor = getStatusColor(order.status);
  const formattedDate = formatDate(order.createdAt);
  
  return (
    <tr className={isHighlighted ? 'highlighted' : ''}>
      <td>{order.id}</td>
      <td style={{ color: statusColor }}>{order.status}</td>
      <td>{formattedDate}</td>
      <td>
        <button onClick={() => onSelect(order.id)}>View</button>
      </td>
    </tr>
  );
});

The catch? The onSelect callback needs to be stable. If the parent creates a new function on every render (which happens in 90% of cases I see), the memo does nothing. More on that in the next section.

In my e-commerce platform's order table, memoizing rows combined with windowing reduced scroll frame drops from 60ms to 8ms on a list of 500 visible orders. That's worth it. But that only happened because I also stabilized the callback with useCallback.

Useless if props change every render

This is the real killer for React.memo. It gives you false confidence. You add the wrapper, feel smart, benchmark sees no improvement, then you move on without investigating why.

Props change every render when:

  • You pass inline objects: <Item config={{ color: 'red' }} />
  • You pass inline arrays: <Item tags={['tag1', 'tag2']} />
  • You pass new function instances: <Item onClick={() => doSomething()} />
  • Props depend on context that changes frequently

Here's the trap in action:

tsx
// Parent component
const Dashboard = () => {
  const [count, setCount] = useState(0);
  
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      
      {/* ❌ This memoization is useless */}
      <MemoizedCard 
        onClick={() => console.log('clicked')} // New function every render
        config={{ theme: 'dark' }} // New object every render
      />
    </>
  );
};

const MemoizedCard = React.memo(({ onClick, config }) => {
  console.log('Card rendered');
  return <div onClick={onClick}>Theme: {config.theme}</div>;
});

Every time count changes, the parent renders. New onClick function, new config object, memo check fails, card renders anyway. The DevTools Profiler will show "MemoizedCard" highlighted in red—meaning it memoized but still rendered because props changed.

I've debugged this mistake in production more times than I'd like to admit. Developers see memoization not working and blame React. The issue is always the unstable props.

Stable props matter more than the wrapper

Here's the real optimization insight: stabilizing props prevents unnecessary renders better than memoization does. React.memo is the last step, not the first.

Let me show you the right way:

tsx
// ✅ Correct approach - stabilize props first
const Dashboard = () => {
  const [count, setCount] = useState(0);
  
  // Stable reference across renders
  const handleCardClick = useCallback(() => {
    console.log('clicked');
  }, []); // Empty deps = never changes
  
  // Stable object reference
  const cardConfig = useMemo(() => ({ theme: 'dark' }), []);
  
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      
      {/* ✅ Now memoization actually works */}
      <MemoizedCard onClick={handleCardClick} config={cardConfig} />
    </>
  );
};

const MemoizedCard = React.memo(({ onClick, config }) => {
  console.log('Card rendered only when theme changes');
  return <div onClick={onClick}>Theme: {config.theme}</div>;
});

Now the memo actually prevents renders. When count changes, Dashboard renders, but handleCardClick and cardConfig have the same reference, so MemoizedCard skips its render.

In my ride-hailing app, I was optimizing a driver map component that re-rendered on every location update (very frequent). Instead of just wrapping it in memo, I extracted the driver marker rows into a separate memoized component and used a location context that only subscribers accessed. That combination reduced unnecessary renders by 95%.

Common mistakes

1. Memoizing without custom equality: React.memo does shallow comparison by default. If you need deep equality, you must pass a custom comparator function. This is rare and usually a sign your component structure needs refactoring.

2. Memoizing components that render once: I've seen memo on components that only exist once in the tree. It does nothing. Measure first.

3. Wrapping memo around expensive calculations inside the component: The component itself might be cheap; the expensive work is in the logic. Use useMemo for the calculation, not memo for the component.

tsx
// ❌ Memoizing won't help if the calculation still runs
const MyComponent = React.memo(({ items }) => {
  const sorted = items.sort((a, b) => a - b); // Still runs every time
  return <div>{sorted.join(',')}</div>;
});

// ✅ Memoize the calculation
const MyComponent = ({ items }) => {
  const sorted = useMemo(() => items.sort((a, b) => a - b), [items]);
  return <div>{sorted.join(',')}</div>;
};

4. Forgetting to stabilize dependencies in useCallback/useMemo: If your dependency array is wrong, you're creating new references constantly anyway. Double-check those deps.

Key takeaways

  • Profile first, optimize second. React DevTools Profiler is non-negotiable. You're looking for components rendering slowly or too often.
  • React.memo only prevents renders if props are actually stable. Unstable props make memo pointless.
  • Long lists with expensive rows are the sweet spot for memoization. Pair memo with useCallback and useMemo for the parent props.
  • Stabilizing props (useCallback, useMemo) matters more than the memo wrapper itself. That's where the real win lives.
  • If you're adding memo to a component that renders once or its props change every render, remove it. You're adding cognitive load for zero benefit.
  • Don't memo unless the measured benefit is real. Premature memoization clutters code and gives a false sense of optimization.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles