Steve Kinney

React.memo in React 19 and the Compiler Era

React 19 brings the React Compiler to general availability, promising to automatically optimize your components for performance. But does that mean React.memo is obsolete? Not quite. While the compiler handles many scenarios that previously required manual memoization, understanding when and how to use React.memo effectively remains crucial for building performant React applications. Let’s explore where React.memo still shines, when modern React makes it redundant, and how to write proper equality comparisons when you need them.

What is React.memo?

React.memo is a higher-order component that prevents unnecessary re-renders by memoizing the result of a component. It compares the current props with the previous props using shallow equality and only re-renders if they’ve changed. Think of it as a performance optimization that says “Hey React, only re-render this component if the props actually changed.”

// Basic usage
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  // Some computationally expensive rendering logic
  return (
    <div>
      {data.map((item) => (
        <Item key={item.id} {...item} />
      ))}
    </div>
  );
});

When a parent re-renders, React will skip re-rendering ExpensiveComponent if its props haven’t changed. Simple enough, right?

The React 19 Compiler Changes Everything

The React Compiler (formerly known as React Forget) automatically identifies opportunities for memoization and applies them behind the scenes. It analyzes your component’s dependency graph and inserts the appropriate optimizations at build time.

// Before: You'd manually wrap this
const UserCard = React.memo(({ user, theme }) => {
  return (
    <div className={`card card--${theme}`}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
});

// After: The compiler handles this automatically
const UserCard = ({ user, theme }) => {
  return (
    <div className={`card card--${theme}`}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
};
// Compiler output includes automatic memoization where beneficial

The compiler is smart about when to apply these optimizations. It won’t memoize components that:

  • Are cheap to render
  • Have props that change frequently
  • Would benefit more from other optimizations
The React Compiler is opt-in for React 19. You’ll need to enable it in your build configuration to take advantage of automatic optimizations.

When React.memo Still Matters

Even with the compiler, there are scenarios where manual React.memo usage remains valuable:

Complex Equality Comparisons

The compiler uses shallow equality by default, but sometimes you need custom comparison logic:

interface ProductCardProps {
  product: {
    id: string;
    name: string;
    specs: Record<string, any>;
    lastUpdated: Date;
  };
  onAddToCart: (id: string) => void;
}

// Only re-render if product data meaningfully changed
const ProductCard = React.memo(
  ({ product, onAddToCart }: ProductCardProps) => {
    return (
      <div>
        <h3>{product.name}</h3>
        <ProductSpecs specs={product.specs} />
        <button onClick={() => onAddToCart(product.id)}>Add to Cart</button>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Custom equality: ignore lastUpdated changes
    const prevProduct = prevProps.product;
    const nextProduct = nextProps.product;

    return (
      prevProduct.id === nextProduct.id &&
      prevProduct.name === nextProduct.name &&
      JSON.stringify(prevProduct.specs) === JSON.stringify(nextProduct.specs) &&
      prevProps.onAddToCart === nextProps.onAddToCart
    );
  },
);
Be careful with

JSON.stringify for deep equality—it’s expensive and doesn’t handle circular references. Consider using a proper deep equality library like Lodash’s isEqual for complex objects.

Third-Party Libraries and Legacy Code

If you’re working with components that haven’t been processed by the compiler (third-party libraries, legacy code), manual memoization can still provide significant benefits:

// Third-party component that isn't compiler-optimized
import { ExpensiveChart } from 'some-chart-library';

const OptimizedChart = React.memo(ExpensiveChart, (prevProps, nextProps) => {
  // Chart only needs to re-render if data actually changed
  return prevProps.data === nextProps.data && prevProps.config === nextProps.config;
});

Fine-Grained Control

Sometimes you know better than the compiler when a component should re-render. This is especially true for components with expensive side effects or complex rendering logic:

interface DataVisualizationProps {
  dataset: DataPoint[];
  filterOptions: FilterConfig;
  renderMode: 'canvas' | 'svg' | 'webgl';
}

const DataVisualization = React.memo(
  ({ dataset, filterOptions, renderMode }: DataVisualizationProps) => {
    // Expensive WebGL rendering that we want to control precisely
    const processedData = useMemo(() => {
      return processDataForVisualization(dataset, filterOptions);
    }, [dataset, filterOptions]);

    return <AdvancedChart data={processedData} mode={renderMode} />;
  },
  (prevProps, nextProps) => {
    // Only re-render if dataset reference changed or critical config changed
    return (
      prevProps.dataset === nextProps.dataset &&
      prevProps.renderMode === nextProps.renderMode &&
      isFilterConfigEqual(prevProps.filterOptions, nextProps.filterOptions)
    );
  },
);

Writing Effective areEqual Functions

When you do need custom equality comparisons, follow these guidelines:

Keep It Simple and Fast

The equality function runs on every potential re-render, so keep it lightweight:

// ✅ Good: Fast property checks
const areEqual = (prevProps, nextProps) => {
  return (
    prevProps.id === nextProps.id &&
    prevProps.status === nextProps.status &&
    prevProps.priority === nextProps.priority
  );
};

// ❌ Bad: Expensive deep equality on every render
const areEqual = (prevProps, nextProps) => {
  return JSON.stringify(prevProps) === JSON.stringify(nextProps);
};

Focus on What Actually Matters

Don’t check properties that don’t affect rendering:

interface TaskItemProps {
  task: {
    id: string;
    title: string;
    completed: boolean;
    createdAt: Date; // Doesn't affect rendering
    internalTracking: any; // Doesn't affect rendering
  };
  onToggle: (id: string) => void;
}

const TaskItem = React.memo(
  ({ task, onToggle }: TaskItemProps) => {
    return (
      <div className={task.completed ? 'completed' : ''}>
        <span>{task.title}</span>
        <button onClick={() => onToggle(task.id)}>{task.completed ? 'Undo' : 'Complete'}</button>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Only check properties that affect rendering
    const prevTask = prevProps.task;
    const nextTask = nextProps.task;

    return (
      prevTask.id === nextTask.id &&
      prevTask.title === nextTask.title &&
      prevTask.completed === nextTask.completed &&
      prevProps.onToggle === nextProps.onToggle
    );
  },
);

Handle Function Props Carefully

Function props are a common source of unnecessary re-renders. Make sure your equality check handles them appropriately:

// If you know the function is stable (wrapped in useCallback), reference equality works
const SimpleComponent = React.memo(
  ({ data, onClick }) => {
    return <button onClick={onClick}>{data.label}</button>;
  },
  (prevProps, nextProps) => {
    return (
      prevProps.data.label === nextProps.data.label && prevProps.onClick === nextProps.onClick // Reference equality for stable functions
    );
  },
);

// If functions might be recreated but are functionally equivalent, you might skip them
const FlexibleComponent = React.memo(
  ({ data, onSubmit }) => {
    return (
      <form onSubmit={onSubmit}>
        <input defaultValue={data.value} />
      </form>
    );
  },
  (prevProps, nextProps) => {
    // Skip function comparison if the data is the same
    return prevProps.data.value === nextProps.data.value;
  },
);

When NOT to Use React.memo

Components That Always Re-render

If your component’s props change on every parent render, memoization adds overhead without benefit:

// ❌ Bad: Timestamp changes every render
const Clock = React.memo(() => {
  return <div>{new Date().toLocaleTimeString()}</div>;
});

// ✅ Better: Just let it re-render
const Clock = () => {
  return <div>{new Date().toLocaleTimeString()}</div>;
};

Cheap Components

For simple components, the memoization overhead might outweigh the benefits:

// ❌ Probably unnecessary
const SimpleLabel = React.memo(({ text, className }) => {
  return <span className={className}>{text}</span>;
});

// ✅ Just let it re-render—it's cheap
const SimpleLabel = ({ text, className }) => {
  return <span className={className}>{text}</span>;
};

When You Have Compiler Optimization

If the React Compiler is handling your component, adding manual React.memo might interfere with its optimizations or create redundant work.

Debugging Memoization Issues

When things aren’t working as expected, React DevTools Profiler is your best friend. But here are some quick debugging techniques:

Log Props Changes

const DebugMemo = React.memo(
  ({ data, config }) => {
    console.log('Component rendering with:', { data, config });
    return <ExpensiveComponent data={data} config={config} />;
  },
  (prevProps, nextProps) => {
    const isEqual = prevProps.data === nextProps.data && prevProps.config === nextProps.config;

    if (!isEqual) {
      console.log('Props changed:', {
        dataChanged: prevProps.data !== nextProps.data,
        configChanged: prevProps.config !== nextProps.config,
      });
    }

    return isEqual;
  },
);

Use React DevTools Profiler

Enable “Record why each component rendered” in React DevTools to see exactly why components are re-rendering, even with memoization.

Install the React DevTools browser extension and use the Profiler tab to identify performance bottlenecks. It’ll show you which components are re-rendering and why.

Real-World Patterns

List Components with Stable Keys

interface TodoListProps {
  todos: Todo[];
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}

const TodoList = React.memo(
  ({ todos, onToggle, onDelete }: TodoListProps) => {
    return (
      <ul>
        {todos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} onToggle={onToggle} onDelete={onDelete} />
        ))}
      </ul>
    );
  },
  (prevProps, nextProps) => {
    // List components benefit from checking array reference equality
    return (
      prevProps.todos === nextProps.todos &&
      prevProps.onToggle === nextProps.onToggle &&
      prevProps.onDelete === nextProps.onDelete
    );
  },
);

Configuration-Heavy Components

interface ChartProps {
  data: DataPoint[];
  options: ChartOptions;
  theme: ThemeConfig;
}

const Chart = React.memo(
  ({ data, options, theme }: ChartProps) => {
    return <ExpensiveChart data={data} options={options} theme={theme} />;
  },
  (prevProps, nextProps) => {
    // For config-heavy components, focus on reference equality
    // assuming parent properly memoizes these objects
    return (
      prevProps.data === nextProps.data &&
      prevProps.options === nextProps.options &&
      prevProps.theme === nextProps.theme
    );
  },
);

The Future of Memoization

As the React Compiler matures and becomes more widely adopted, manual memoization will become less necessary for most use cases. However, React.memo will remain valuable for:

  • Complex components with specific optimization needs
  • Integration with non-React systems
  • Fine-grained performance tuning
  • Working with legacy code that can’t use the compiler

The key is understanding when the compiler has your back and when you need to step in with manual optimizations.

Best Practices Summary

  1. Default to letting the compiler handle it if you’re using React 19 with the compiler enabled
  2. Use React.memo for custom equality logic when shallow comparison isn’t sufficient
  3. Keep areEqual functions fast and focused on rendering-relevant properties
  4. Profile before optimizing to ensure you’re solving real performance problems
  5. Don’t memoize everything—cheap components often don’t benefit from memoization
  6. Be careful with function props—ensure they’re stable or handled appropriately in comparisons

Last modified on .