How to Optimize useCallback Hook in React

This article provides a practical guide on how to optimize the useCallback hook in React to improve application performance. You will learn the core purpose of useCallback, how to identify when it is actually beneficial to use, and how to avoid common anti-patterns that can degrade performance instead of improving it.

Understand When to Use useCallback

The useCallback hook is designed to cache (memoize) a function definition between re-renders. However, caching has a performance cost. You should not wrap every function in useCallback. It is only beneficial in two main scenarios:

  1. Passing callbacks to memoized child components: If a child component is wrapped in React.memo and receives a function as a prop, using useCallback prevents the child from re-rendering unless the function’s dependencies change.
  2. The function is a dependency in other hooks: If the function is used inside a useEffect, useMemo, or another useCallback dependency array, memoizing it prevents infinite loops or unnecessary executions.

Pair useCallback with React.memo

Using useCallback on a function passed to a standard component is useless if that child component re-renders anyway. To optimize performance, always pair useCallback with React.memo on the receiving component.

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

// Optimized child component
const Button = React.memo(({ handleClick, children }) => {
  console.log(`Rendering ${children}`);
  return <button onClick={handleClick}>{children}</button>;
});

export default function ParentComponent() {
  const [count, setCount] = useState(0);

  // Memoized function
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // Empty dependency array means this function never changes

  return (
    <div>
      <p>Count: {count}</p>
      <Button handleClick={increment}>Increment</Button>
    </div>
  );
}

Minimize Dependency Array Changes

A useCallback hook will recreate the function every time any dependency in its dependency array changes. To optimize this, keep the dependencies to an absolute minimum.

Use Functional State Updates

If your callback only updates a state based on its previous value, use a functional state update. This allows you to remove the state variable from the dependency array entirely.

Inefficient:

// Recreates the function every time 'count' changes
const increment = useCallback(() => {
  setCount(count + 1);
}, [count]); 

Optimized:

// Function is created only once on mount
const increment = useCallback(() => {
  setCount((prevCount) => prevCount + 1);
}, []); 

Avoid useCallback for Simple Inline Logic

For lightweight operations or components that do not have expensive rendering logic, the overhead of setting up useCallback (instantiating the hook, running dependency checks on every render) is often higher than the cost of redefining a raw JavaScript function. Keep your code simple and omit useCallback unless you observe measurable performance bottlenecks.