When to Use the React useCallback() Hook?

In React, functions created inside a component are recreated on every re-render, and as a result, have a different reference in memory. This means that the function is seen as a different function by React, even though it has the same body. This can cause unnecessary re-renders in situations you don't expect, such as the following:

  1. Passing a function as a prop to a memoized component;
  2. Passing a function to the dependency array of some hook;
  3. Passing a function via context.

In such cases, useCallback can be used to memoize/cache the function to prevent unnecessary re-renders. In this way, the function is only called again if the provided dependencies change.

Please note that there is no real benefit of wrapping a function with useCallback() in other cases than those mentioned above. However, you may still do so without causing any major issues.

That being said, you should consider not using useCallback() when the function in question:

  • Is small and does not cause any significant performance issues or re-renders in the child component;
  • Relies on values that change on every render, as this would require a new function to be created on every render anyway;
  • Is only used once, and is not reused in multiple places within the component or its children.

Passing Function as a Prop to Memoized Component

When passing a function as a prop to a memoized component (i.e. component wrapped in memo or useMemo()), you must wrap the function with useCallback(). Otherwise, it will cause the memoized component to be re-rendered unnecessarily.

For example, consider the following top-level component that has a reactive value used for storing the selected state of "Dark mode":

// App.jsx
import React, { useState } from 'react';
import Parent from './Parent';

const App = () => {
  const [isDark, setIsDark] = useState(false);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={(e) => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <Parent theme={isDark ? 'dark' : 'light'} />
    </>
  );
};

export default App;

The "theme" is determined based on user selection of "Dark mode" and passed down as a prop to the following "Parent" component:

// Parent.jsx
import React, { useCallback, useState } from 'react';
import Child from './Child';

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

  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Theme: {theme}</p>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
    </div>
  );
}

export default Parent;

This "Parent" component manages a reactive "counter" that is updated by a handler function. This handler function is passed down to a "Child" component that's wrapped in memo (higher order component):

// Child.jsx
import React, { memo } from 'react';

const Child = ({ onClick }) => {
  console.log('rendered');

  return (
    <button onClick={onClick}>
      Increment count
    </button>
  );
};

export default memo(Child);

In this example, everytime "Dark mode" is toggled, it causes a re-render of the Parent component. This in-turn creates a new handler function on every re-render. Since this function is being passed as a prop to the Child component, it causes it to be re-rendered, even though it is memoized.

To fix this issue, you can simply wrap the handleClick function with useCallback(), for example, like so:

const handleClick = useCallback(() => {
  setCount(count + 1);
}, [count]);

By wrapping handleClick with useCallback(), React is able to use the cached version of the function between re-renders. As a result, the Child component is only re-rendered when the value of "count" changes.

Passing Function to a Dependency Array of a Hook

When passing a function to the dependency array of a hook, you should wrap the function with useCallback() to ensure that the hook is only executed when necessary. Otherwise, the hook will be executed on every render.

For example, consider the following component that has a reactive value used for storing the selected state of "Dark mode":

// App.jsx
import React, { useEffect, useState } from "react";

const App = () => {
  const [isDark, setIsDark] = useState(false);

  const foo = () => {
    console.log('foo was called');
  };

  useEffect(() => {
    foo();
  }, [foo]);

  return (
    <label>
      <input
        type="checkbox"
        checked={isDark}
        onChange={(e) => setIsDark(e.target.checked)}
      />
      Dark mode
    </label>
  );
};

export default App;

In this example, everytime you toggle "Dark mode", it triggers a re-render, which causes the "foo()" function to be recreated. When this happens, the useEffect() callback is executed because React sees a different "foo()" function (as it's reference in memory changes).

To fix this issue, you can simply wrap "foo()" with useCallback(), or as an alternative, you can also move the function definition inside the useEffect() callback.

Passing Function via Context

When passing a function via context, it can trigger re-renders of all components consuming that context when the function or its context changes. This can have a performance impact when updates are frequent or if many components rely on it. In such a case, you can wrap the function with the useCallback() hook to avoid unnecessary re-renders.

For example, let's suppose you have the following context:

// MyContext.js
import { createContext } from 'react';

export default createContext();

Using this context, let's suppose, you pass a function down the component hierarchy from the App component:

// App.jsx
import React, { useState } from 'react';
import MyContext from './MyContext';
import Parent from './Parent';

const App = () => {
  const [isDark, setIsDark] = useState(false);
  const [totalItems, setTotalItems] = useState(1000);

  const createItems = () => Array.from({ length: totalItems }, (_, i) => i + 1);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={(e) => setIsDark(e.target.checked)}
        />
        Dark mode
      </label>
      <MyContext.Provider value={createItems}>
        <Parent />
      </MyContext.Provider>
    </>
  );
};

export default App;

The App component includes a checkbox for toggling "dark mode", which triggers a re-render, however, it does not affect the Parent component as it is memoized:

// Parent.jsx
import React, { memo } from 'react';
import Child from './Child';

const Parent = () => <Child />;

export default memo(Parent);

This Parent component renders the following Child component:

// Child.jsx
import React, { useContext } from 'react';
import MyContext from './MyContext';

const Child = () => {
  const createItems = useContext(MyContext);

  console.log('rendered');

  return (
    <ul>
      {createItems().map((item, idx) => (
        <li key={idx}>{item}</li>
      ))}
    </ul>
  );
};

export default Child;

When the "dark mode" option is toggled in App component, even though the Parent is not re-rendered, the Child still is. This is because it consumes the function from the context declared in the App component. When a re-render is triggered in the App component, the createItems() function is re-created, resulting in it having a different reference in memory. This in-turn makes the Child component re-render unnecessarily.

To fix this issue, you can simply wrap "createItems()" in the App component with useCallback(), for example, like so:

const createItems = useCallback(() => (
  Array.from({ length: totalItems }, (_, i) => i + 1)
), [totalItems]);

This post was published (and was last revised ) by Daniyal Hamid. Daniyal currently works as the Head of Engineering in Germany and has 20+ years of experience in software engineering, design and marketing. Please show your love and support by sharing this post.