Stackvora.tech

React Performance: useMemo, useCallback and When NOT to Use Them

Optimizing React component re-renders is a critical aspect of building high-performance web applications. While React's reconciliation algorithm is highly…

Optimizing React component re-renders is a critical aspect of building high-performance web applications. While React's reconciliation algorithm is highly efficient, unnecessary computations or component re-renders can still lead to UI jank and a poor user experience. React provides the useMemo and useCallback hooks to help manage these scenarios by preventing redundant work and preserving referential equality.

This article delves into the mechanics of useMemo and useCallback, explaining when and, crucially, when not to use them, supported by concrete examples and profiling advice.

Understanding React Component Re-renders

Before diving into optimization hooks, it's essential to understand React's re-rendering triggers. A React component re-renders under the following conditions:

  • State Changes: The component's own state (managed by useState or useReducer) is updated.
  • Props Changes: Its parent component re-renders and passes new props, or the parent itself re-renders causing child components to re-render even if props are shallow-equal (unless memoized).
  • Context Changes: A context value consumed by the component changes.
  • Parent Re-renders: If a parent component re-renders, all its children (and their children, recursively) will re-render by default, regardless of whether their own state or props have changed, unless explicitly optimized.

React.memo, useMemo, and useCallback are tools to prevent re-renders or re-computations when the inputs to a component or a calculation haven't truly changed.

React.memo: Optimizing Component Re-renders

React.memo is a higher-order component (HOC) that memoizes a functional component. It prevents a component from re-rendering if its props have not shallowly changed. This is the primary mechanism for preventing unnecessary child component re-renders when their parent re-renders.

import React from 'react';

const ExpensiveComponent = ({ data, onClick }) => {
  // Simulate an expensive render
  console.log('ExpensiveComponent rendering...');
  let sum = 0;
  for (let i = 0; i < 1000000; i++) {
    sum += i;
  }
  return (
    <div>
      <p>Data: {data.value}</p>
      <button onClick={onClick}>Click Me</button>
      <p>Expensive sum: {sum}</p>
    </div>
  );
};

// Memoize the component
const MemoizedExpensiveComponent = React.memo(ExpensiveComponent);

// Usage in a parent component
function ParentComponent() {
  const [count, setCount] = React.useState(0);
  const data = { value: 'static data' }; // This object reference does not change

  // If onClick is not memoized, MemoizedExpensiveComponent will still re-render
  const handleClick = () => {
    console.log('Button clicked!');
  };

  return (
    <div>
      <h1>Parent Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment Parent</button>
      <MemoizedExpensiveComponent data={data} onClick={handleClick} />
    </div>
  );
}

In the example above, if handleClick is not wrapped with useCallback, MemoizedExpensiveComponent will still re-render when ParentComponent's count state updates. This is because a new function reference for handleClick is created on every ParentComponent render, causing React.memo to see a "changed" prop.

When to use React.memo:

  • Components that render frequently with the same props.
  • Components with a complex or expensive rendering logic.
  • Components that receive props that are likely to be referentially stable (e.g., primitive values, memoized objects/arrays/functions).

When not to use React.memo:

  • Components that re-render naturally due to their own state changes.
  • Components that rarely re-render or have very simple rendering logic. The overhead of prop comparison by React.memo might outweigh the benefit.
  • Components that always receive new, complex object/array props, unless you provide a custom comparison function as the second argument to React.memo (e.g., React.memo(Component, (prevProps, nextProps) => ...)).

useCallback: Memoizing Functions

useCallback(callback, dependencies) returns a memoized version of the callback function that only changes if one of the dependencies has changed. It's primarily used to maintain referential equality of functions passed as props to memoized child components, preventing unnecessary re-renders of those children.

import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendering...');
  return <button onClick={onClick}>Click Child</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('Hello');

  // This function is recreated on every render if not memoized.
  // With useCallback, it only changes if 'count' changes.
  const handleClick = useCallback(() => {
    console.log(`Child button clicked. Count: ${count}`);
    // Example: update parent state based on count
    setMessage(`Clicked at count: ${count}`); 
  }, [count]); // Dependency array: only re-create handleClick if count changes

  return (
    <div>
      <h1>Parent Count: {count}</h1>
      <p>Message: {message}</p>
      <button onClick={() => setCount(count + 1)}>Increment Parent</button>
      <ChildComponent onClick={handleClick} />
      <button onClick={() => setMessage('New Message')}>Change Message (Parent)</button>
    </div>
  );
}

In this example, changing the parent's message state will not cause ChildComponent to re-render because handleClick (which depends only on count) retains its referential identity. If count changes, handleClick will be recreated, and ChildComponent will re-render.

When to use useCallback:

  • When passing callback functions to React.memo-wrapped child components to prevent unnecessary re-renders.
  • When a function is a dependency in another hook's dependency array (e.g., useEffect, useMemo, or another useCallback) to avoid re-running effects or re-computing values unnecessarily.
  • To preserve referential equality for functions that are part of a shared context or global state management system.

When not to use useCallback:

  • For functions passed to non-memoized components. The child will re-render anyway when its parent does.
  • For simple functions that are not passed down as props or used in dependency arrays. The overhead of memoization might be greater than the cost of recreation.
  • When the function's dependencies change very frequently, making the memoization ineffective.

useMemo: Memoizing Values

useMemo(factory, dependencies) returns a memoized value. It only recomputes the memoized value when one of the dependencies has changed. This is useful for expensive calculations or for creating referentially stable objects/arrays.

import React, { useState, useMemo } from 'react';

function calculateExpensiveValue(num) {
  console.log('Calculating expensive value...');
  let result = 0;
  for (let i = 0; i < num * 1000000; i++) {
    result += i;
  }
  return result;
}

const DisplayValue = React.memo(({ value }) => {
  console.log('DisplayValue rendering...');
  return <p>Expensive Value: {value}</p>;
});

function App() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(1);

  // This calculation is performed only when 'multiplier' changes.
  // Changing 'count' will not trigger recalculation.
  const memoizedExpensiveValue = useMemo(() => {
    return calculateExpensiveValue(multiplier);
  }, [multiplier]); // Dependency array

  // If we want to pass an object or array with referential stability
  const memoizedProps = useMemo(() => ({
    message: "Memoized Message",
    id: 123
  }), []); // Empty dependency array means it's created once

  return (
    <div>
      <h1>Count: {count}</h1>
      <p>Multiplier: {multiplier}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setMultiplier(multiplier + 1)}>Increment Multiplier</button>

      <DisplayValue value={memoizedExpensiveValue} />
      {/* Example: passing a memoized object to a child */}
      <AnotherComponent config={memoizedProps} /> 
    </div>
  );
}

// AnotherComponent would ideally be React.memo-wrapped if config stability matters
const AnotherComponent = React.memo(({ config }) => {
  console.log('AnotherComponent rendering...');
  return <p>Config message: {config.message}, ID: {config.id}</p>;
});

Here, memoizedExpensiveValue is only recalculated when multiplier changes. Updates to count, which don't affect multiplier, won't trigger the heavy computation, and DisplayValue will not re-render (assuming its value prop is stable).

When to use useMemo:

  • For computationally expensive calculations where inputs (dependencies) don't change often.
  • To prevent referential equality breaks for objects or arrays passed as props to React.memo-wrapped child components or used in dependency arrays of other hooks (e.g., useEffect).
  • When creating values that are used as dependencies for useEffect or useCallback to prevent unintended re-executions.

When not to use useMemo:

  • For simple values or calculations that are not expensive. The overhead of memoization can be greater than the cost of recalculation.
  • When the memoized value is not passed to a memoized child component or used in a dependency array.
  • When dependencies change very frequently, making memoization ineffective.

Rules of Thumb and Profiling

The golden rule for React performance optimization is: profile first. Don't blindly apply useMemo, useCallback, or React.memo everywhere. They come with their own overhead (memory usage for storing memoized values, and CPU for comparing dependencies). Their benefits are realized only when the cost of comparison is less than the cost of re-rendering or re-computation.

Use the React DevTools Profiler:

  1. Open Chrome DevTools.
  2. Go to the "Profiler" tab.
  3. Click the "Record" button (the circle icon).
  4. Interact with your application to trigger the suspected performance issue.
  5. Stop recording.
  6. Examine the flame graph or ranked chart. Select a component and look at the "Why did this render?" section on the right sidebar. This provides invaluable insights into what triggered a re-render.

If the profiler indicates that a component is re-rendering frequently without its props changing (and it's an expensive component), then React.memo is a candidate. If a memoized child component is still re-rendering due to function or object/array props, then useCallback or useMemo might be necessary for those props.

Consider the following hierarchy for optimization:

  1. React.memo: Apply this to components that are expensive to render and receive stable props.
  2. useCallback: Use this for functions passed to React.memo-wrapped children.
  3. useMemo: Use this for expensive calculations or for creating referentially stable objects/arrays passed to React.memo-wrapped children or used in dependency arrays.

Common Pitfalls

  • Incorrect Dependency Arrays: Forgetting to include a dependency, or including too many dependencies, can negate the benefits of useCallback/useMemo. A missing dependency can lead to stale closures (using old values), while too many dependencies can cause unnecessary re-creations/re-computations. Always ensure your dependency array accurately reflects all values from the component's scope that are used inside the memoized function or value.
  • Premature Optimization: Applying these hooks to every component or value without profiling can introduce unnecessary complexity and overhead. Simple components rarely benefit.
  • Shallow Comparison Limitations: React.memo and the default comparison for useCallback/useMemo dependencies perform shallow comparisons. If your props/dependencies are deeply nested objects, changes within those nested objects won't be detected, leading to missed updates or ineffective memoization. For deep comparisons, you'd need a custom comparison function for React.memo or careful structuring of your state.
  • Impact on Readability: Overuse of these hooks can clutter your code, making it harder to read and maintain without clear performance benefits.

Back to the knowledge base · Ask the AI assistant