Skip to content
Shea Long

12 min read

React useCallback: A Quick Guide

Discover how React’s useCallback hook can help you optimize performance and reduce unnecessary re-renders in your app.

Introduction

I find that a decent amount of performance issues in React apps often come down to unnecessary re-renders.

Let's say you're working on an app with a performance issue. Sometime after debugging you come across a component that is re-rendering multiple times for no reason, wrap it in a React.memo and move on, only to find that it's still re-rendering.

The component is only taking two props:

  • label (type string)
  • onButtonPress for onClick events (type function)

The culprit is the prop onButtonPress, whose reference in memory is changing when the component's parent re-renders. Implementing the useCallback hook will help achieve the goal of preventing this component from re-rendering and further optimize your app.

In this tutorial, I’ll explain how useCallback works and when it’s useful in your React projects. By the end, you’ll have a solid understanding of when and why to use this hook to optimize performance and make your app more efficient.

What is useCallback

In plain terms, React's useCallback hook allows functions to retain the same reference in memory (memoize/cache) between renders. When comparing props to evaluate a re-render, or dependencies in a hook's dependency array, React uses the Object static method Object.is to determine if the variables have changed.

In JavaScript, functions are Objects , which means that natively, no two functions can be equal each other. This makes useCallback a great tool because it allows a function written in a React component to be equal to itself on re-render, given that the values in its dependency array don't change.

Proper usage can prevent unnecessary renders, which can optimize performance within an application.

Improper usage can add to code complexity, possibly worse performance, and odd bugs when functions are executed with outdated parameters.

Key features of useCallback:

  • Memoization: React stores the function and only updates it when necessary.
  • Dependency array: The function is re-created only if one or more of the values in the dependency array change.
  • Better Performance: Helps reduce unnecessary re-renders, especially in large or complex components.

Requirements for useCallback:

  • Functional components only: useCallback is a hook, so it must be used within functional components.
  • Top-level hook: Always place hooks at the top level of your component or custom hook, not inside loops or conditionals.
  • Memoizing passed functions: Useful when passing functions as props, particularly to React.memo wrapped components.
  • Accurate dependencies: Ensure all variables the function relies on are included in the dependency array to avoid stale data.
  • Avoid overuse: Not all functions need to be memoized. Overuse can add complexity without meaningful performance gains.

Syntax for useCallback

The useCallback hook takes two arguments:

  • Callback function: Contains the logic that will execute when called.
  • Dependency array: Controls when the function is re-created. If the array is empty, the function reference will remain the same throughout the component's lifecycle.

Simple Usage:


import { useCallback } from "react";

// Dependent on variables a and b
const memoizedCallback = useCallback(() => {
// Logic
}, [a, b]);

// No dependent variables, function will not be recreated
const memoizedCallback = useCallback(() => {
// Logic
}, []);

Inline Function:


import { useCallback } from "react";

const FunctionalComponent = ({ dependency1 = 1, dependency2 = 2 }) => {

    const memoizedCalculation = useCallback(() => {
    	return dependency1 + dependency2;
    }, [dependency1, dependency2]);

    return <div>useCallback Example: {memoizedCalculation()}</div>

};

Passing a Function:


import { useCallback } from "react";

const FunctionalComponent = ({ dependency1 = 1, dependency2 = 2 }) => {

    const handleCalculation = (dependency1, dependency2) => {
    	return dependency1 + dependency2;
    }

    const memoizedCalculation = useCallback(
    	() => handleCalculation(dependency1, dependency2),
    [dependency1, dependency2]
    );

    return (<div>useCallback Example: {memoizedCalculation()}</div>)

};

Custom Hook:


import { useCallback } from "react";

const useCustomHook = ({ dependency1 = 1, dependency2 = 2 }) => {
const memoizedHandleCalculationOnClick = useCallback(() => {
return dependency1 + dependency2;}, [dependency1, dependency2]
);

    return memoizedHandleCalculationOnClick;

}

When to Use useCallback

In my opinion, useCallback is a performance optimization tool that should only be used when you discover performance problems in your app, or if you're 100% sure you writing a function that will need to use useCallback.

If you pre-optimize and wrap your functions in useCallback before you've finished working on a feature, you might forgot to update the dependency array if and when the logic changes and you're developing. I've seen this lead to bugs that can take a while to discover because technically, everything is working correctly, but the functions are executing with outdated variables.

Consider using useCallback in these cases:

  • Passing Functions as Props: If you’re passing a function as a prop to a child component, especially one wrapped in React.memo, memoizing the function helps prevent the child from re-rendering unnecessarily.
  • Event Handlers in Complex Components: In large components with multiple event handlers, memoizing functions can prevent excessive re-creation and ensure smoother performance.

Expensive Operations Argument

In the above list of considerations on when to use useCallback, I did not mention avoiding expensive calculations or function calls as a possible scenario.

I see a lot of guides and tutorials on useCallback that mention if the the function logic "performs costly calculations" or "makes API calls", wrapping it the function is useCallback will have performance benefits.

This argument makes zero sense to me:

  • If the function is only called on mount or as the side-effect of an event handler, then why does it matter if the logic that executes is expensive and costly? When React recreates a non-memoized function on re-render, it does not execute the function and run the logic, because the function is not being called on re-render.
  • If the function is called on every render, once again, why does it matter if the logic is costly? This function has to run on every render, and by wrapping it in useCallback, you're making React compare the dependency array in addition to executing the function - when it was going to execute anyway.

Consider the following:


import { useCallback, useEffect, useState, useRef } from "react";

const usePrevious = (value) => {
const ref = useRef();

    useEffect(() => {
    	ref.current = value;
    }, [value]);

    return ref.current;

};

const CountExample = () => {
const [count, setCount] = useState(0);

    const memoizedCalculation = useCallback(() => {
    	console.log("memoizedCalculation");
    	return 1 + 2;
    }, [1, 2]);

    const prevMemoizedCalculation = usePrevious(memoizedCalculation);
    const functionsAreEqual = prevMemoizedCalculation === memoizedCalculation;

    console.log("functionsAreEqual", functionsAreEqual);

    return (
        <div>
          <p>count: {count}</p>
          <p>{memoizedCalculation()}</p>

          <button
            onClick={() => {
              setCount((prev) => prev + 1);
            }}
      >
    	    Increment Count
          </button>
        </div>
      );

};

In the above example:

  • usePrevious hook is declared, which stores the previous value of a variable for use on the next render
  • count and setCount useState is declared with a simple increment counter function on button click, as a way to force the component to re-render
  • memoizedCalculation useCallback hook is declared, with its dependencies being both primitive values and constant (ensuring React will not return a new function on re-render)

Notice that the usePrevious is storing the previous value of the memoizedCalculation calculation function, which essentially is () => {}

After the component mounts and the Increment Count button is clicked:

  • The state increases by its values by 1
  • The log for functionsAreEqual will write true to the console showing that React properly return the same function instance between renders
  • Most importantly, memoizedCalculation gets called and the logic runs returning the same value. The log for memoizedCalculation will write to the console on every re-render.

The memoizedCalculation logging on every render shows the difference between useCallback and useMemo. useCallback will not optimize performance if the function is being called on every render.

How to Use useCallback

I typically use useCallback in two scenarios:

  • Prop Functions: When passing function props to child components that I don't want to re-render unnecessarily.
  • Custom Hooks: When returning functions from hooks that require stable function references.

1. Using useCallback in a Functional Component

You will most likely find yourself using useCallback to prevent unnecessary re-renders of child components when passing functions as props. By memoizing the function, it ensures that the child component doesn’t re-render unless necessary.


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

const ChildComponent = memo(({ onClick, label }) => {
console.log('Child re-rendered');
return <button onClick={onClick}>{label}</button>;
});

const ParentComponent = () => {
const [count, setCount] = useState(0);

// Memoizing the click handler to prevent unnecessary re-renders
const onButtonPress = useCallback(() => {
console.log(`Button clicked, count is ${count}`);
}, [count]);

return (

<div>
  <p>Count: {count}</p>
  <button onClick={() => setCount(count + 1)}>Increment Count</button>
  <ChildComponent onClick={onButtonPress} label="Click Me" />
</div>
); 

In this example, onButtonPress is memoized to ensure that the child component (ChildComponent) doesn't re-render every time the parent component re-renders. The function is only re-created when the count changes.

2. Using useCallback in a Custom Hook

Custom hooks can also benefit from useCallback, especially when a function inside the hook depends on changing data. By memoizing the function, it ensures consistent behavior without unnecessary re-creation.


import { useState, useCallback } from 'react';

const useToggle = (initialValue) => {
const [value, setValue] = useState(initialValue);

// Memoizing the toggle function to prevent unnecessary re-renders
const toggle = useCallback(() => {
setValue((prevValue) => !prevValue);
}, []);

return [value, toggle];
};

This custom hook memoizes the toggle function, ensuring that it remains stable across re-renders unless dependencies change.

Incorrect Usage of useCallback

Using useCallback improperly can lead to unnecessary complexity without meaningful performance gains. Below are some common examples of incorrect usage.

1. Over-Memoizing Simple Functions

Memoizing simple functions adds overhead without any real benefit. This can lead to unnecessary complexity, and maintainability issues in your codebase.


import { useCallback } from 'react';

const FunctionalComponent = () => {
const logMessageOnButtonPress = useCallback(() =>
{ console.log('Look mah no hands!'); }, []);

    return <button onClick={logMessageOnButtonPress}>Log Message</button>

}

In this case, logMessageOnButtonPress is a simple function that doesn’t need memoization. Since it doesn't cause a performance bottleneck, wrapping it in useCallback adds unneeded complexity.

2. Forgetting Dependencies

A frequent mistake is forgetting to include all necessary dependencies in the array. This can result in bugs where the memoized function uses outdated values.

Quick Note on this:

You do not need to always add all dependencies to the dependency array.

On several occasions I have used useCallback and the function logic did not care if some of the dependencies changed (meaning those dependencies could have the same value they did on mount every time the function is executed).


import { useCallback } from 'react';

const ParentComponent = () => {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
console.log(`Count is: ${count}`); // Depends on count
}, []); // Missing count in the dependencies

return (

<div>
  <button onClick={handleClick}>Log Count</button>
</div>
); }; 

3. Functions That Never Change

If a function does not depend on any external variables or props, it should not be memoized using useCallback. Memoizing such functions adds unnecessary overhead without offering performance improvements.


import { useCallback } from 'react';

const staticFunction = useCallback(() => {
console.log('This function never changes');
}, []); // No dependencies, no need for memoization

const handleChange = useCallback((e) => {
setValue(e.target.value); }, []); // No real need to memoize simple function

Performance Considerations

While useCallback can optimize your React app by reducing unnecessary function re-creation, it’s important to remember that it introduces its own performance overhead. Every time a component renders, React must compare the dependencies to decide whether to return a new function or reuse the existing one. This comparison itself has a cost.

Here are some key points for you to keep in mind:

  • Minimal Gains for Simple Functions: If a function is lightweight or doesn’t trigger re-renders, the performance gains from using useCallback are minimal. In such cases, it might be better to skip memoization altogether.
  • Avoid Overusing: Overusing useCallback can lead to more complex code without meaningful performance improvements. Only memoize functions when necessary, such as when passing them as props to React.memo wrapped child components.
  • Careful Dependency Management: Ensure the dependency array is accurate. If dependencies are mismanaged, useCallback can introduce bugs or create stale closures, which could degrade performance rather than improve it.
  • Profiling Before Optimizing: Always measure and profile your app’s performance before deciding to use useCallback. React’s built-in DevTools profiler can you help identify re-renders and performance bottlenecks.

Closing Thoughts

useCallback is a powerful tool for optimizing performance in React applications, but it’s not a silver bullet. Like most optimizations, it should be used thoughtfully and only when necessary. Overusing it can result in harder-to-read code with minimal performance gains.

When used correctly, useCallback can prevent unnecessary re-renders and boost your app's performance. However, always test whether it truly benefits your specific use case before relying on it.

React’s optimization hooks like useCallback, useMemo, and useRef are best used in moderation, striking the right balance between simplicity and performance.