DEV Community

Cover image for Optimizing React Re-Renders: When to Use useMemo, useCallback, and React.memo
DEVANSHU PATIL
DEVANSHU PATIL

Posted on AI-assisted

Optimizing React Re-Renders: When to Use useMemo, useCallback, and React.memo

Optimizing React Re-Renders: When to Use useMemo, useCallback, and React.memo

title: "Optimizing React Re-Renders: When to Use useMemo, useCallback, and React.memo"
published: true
published_at: "2026-11-16T09:00:00+05:30"
description: "A deep dive into React performance optimization. Learn how to diagnose re-renders using React DevTools, understand referential equality, refactor state locations before memoizing, and apply useMemo, useCallback, and React.memo effectively without falling into premature optimization traps."
tags: [react, javascript, performance, frontend]
ai_disclosure_level: some_ai

Introduction

Performance optimization in React is often misunderstood. Developers frequently wrap every function in useCallback, every computed value in useMemo, and every component in React.memo out of fear of slow re-renders. However, indiscriminate memoization introduces its own overhead—memory allocation, garbage collection pressure, and increased code complexity.

To optimize React applications effectively, you must understand how JavaScript handles referential equality, how the React reconciliation engine works, and how to use profiling tools to target actual bottlenecks rather than guessing.

Understanding React Re-Renders

A re-render in React occurs when a component's state changes, its props change, or its parent component re-renders. By default, when a parent component re-renders, React recursively re-renders all of its child components, regardless of whether their specific props have changed.

Most of the time, this default behavior is completely fine. React's virtual DOM reconciliation is remarkably fast. Optimization is only necessary when a component tree is deeply nested, contains expensive calculations, or includes complex DOM manipulations that cause noticeable jank.

The Cost of Premature Optimization

Applying memoization primitives carries a cost:

  1. Memory overhead: useMemo and useCallback cache values between renders, retaining references in memory.
  2. CPU overhead: On every render, React must execute useMemo or useCallback and perform a shallow comparison of dependency arrays. If the dependency array is large or evaluated frequently, this can exceed the cost of simply re-running the calculation or re-creating the function.
  3. Cognitive overhead: Code readability decreases significantly when memoization wrappers obscure the data flow.

Referential Equality Pitfalls

JavaScript compares objects, arrays, and functions by reference, not by value. This is the root cause of most unwanted re-renders.

// These two arrays are structurally identical, but referentially distinct
const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(a === b); // false
Enter fullscreen mode Exit fullscreen mode

When passing inline objects, arrays, or functions as props to a child component, a new reference is created on every single parent render:

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

  // A new function reference is created every time Parent renders
  const handleClick = () => {
    console.log('Clicked');
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment: {count}</button>
      {/* Child will re-render on every button click because handleClick is a new reference */}
      <Child onClick={handleClick} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Profiling Renders with React DevTools

Never optimize without data. The Profiler tab in React DevTools is the definitive tool for identifying performance bottlenecks.

Steps to Profile:

  1. Open React DevTools in your browser and navigate to the Profiler tab.
  2. Click the record button (blue circle).
  3. Perform the user action that feels slow or laggy.
  4. Stop recording.
  5. Inspect the Flamegraph or Ranked chart to identify which components took the longest time to render.

Additionally, enable "Highlight updates when components render" in the Profiler settings to visually inspect which components flash on the screen during state updates.

Refactoring State Location Before Memoization

Before reaching for useMemo, useCallback, or React.memo, evaluate your state architecture. Often, re-render issues stem from state being placed too high in the component tree.

Anti-Pattern: Lifting State Too High

function Dashboard() {
  const [theme, setTheme] = useState('light');
  const [searchQuery, setSearchQuery] = useState('');

  return (
    <div className={theme}>
      {/* Typing in the search input causes the entire Dashboard to re-render, 
          including heavy components that do not care about searchQuery */}
      <SearchInput value={searchQuery} onChange={setSearchQuery} />
      <HeavyAnalyticsDashboard theme={theme} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Solution: Colocating State and Component Composition

By pushing state down to the smallest possible common ancestor, or by utilizing component composition (passing components as children), you can prevent unnecessary propagation of re-renders without a single line of memoization.

function SearchSection() {
  const [searchQuery, setSearchQuery] = useState('');
  return <SearchInput value={searchQuery} onChange={setSearchQuery} />;
}

function Dashboard({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <div className={theme}>
      {/* HeavyAnalyticsDashboard is rendered by the parent of Dashboard,
          so it does not re-render when theme changes or when SearchSection updates */}
      {children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When to Use Memoization Hooks

When structural refactoring is not enough, React provides three specific APIs to control re-renders.

1. React.memo

React.memo is a higher-order component that memoizes the result of a component render. It performs a shallow comparison of props by default.

import React from 'react';

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  console.log('ExpensiveList rendered');
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});
Enter fullscreen mode Exit fullscreen mode
  • Use when: A pure component renders often with the exact same props and rendering is demonstrably slow.
  • Do not use when: Props change on almost every render; the shallow comparison check is wasted overhead.

2. useCallback

useCallback caches a function definition between renders.

import { useState, useCallback } from 'react';

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

  const handleAction = useCallback(() => {
    console.log('Action triggered');
  }, []); // Empty dependency array means this function reference never changes

  return (
    <Child onAction={handleAction} />
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Use when: Passing callback functions down to optimized child components (React.memo) whose render performance depends on referential equality.
  • Do not use when: Passing callbacks to native DOM elements (e.g., <button onClick={handleAction}>), as DOM elements do not care about reference stability.

3. useMemo

useMemo caches the result of an expensive calculation.

import { useMemo, useState } from 'react';

function DataProcessor({ data }) {
  const [filter, setFilter] = useState('');

  const filteredData = useMemo(() => {
    console.log('Running expensive filtering...');
    return data.filter(item => item.name.includes(filter));
  }, [data, filter]);

  return <div>{/* Render filtered data */}</div>;
}
Enter fullscreen mode Exit fullscreen mode
  • Use when: Performing heavy computational loops, data transformations, or maintaining referential stability for complex objects passed as props.
  • Do not use when: Calculating simple primitives or running lightweight transformations where the CPU cost of useMemo outweighs the execution cost.

Summary

Optimization in React should be systematic rather than reactive. Follow this decision tree before writing optimization code:

  1. Profile: Use React DevTools to confirm a performance bottleneck exists.
  2. Architect: Check if state can be moved closer to where it is used, or if component composition can isolate updates.
  3. Measure: Apply React.memo, useCallback, or useMemo only when necessary, and verify the performance improvement using the profiler.

Top comments (0)