DEV Community

Cover image for Understanding the useCallback hook
Vasil Vasilev
Vasil Vasilev

Posted on • Edited on

Understanding the useCallback hook

Official Definition

According to the React official documentation as of the time of writing this article, useCallback is a React Hook that lets you cache a function definition between re-renders.
Thus, we try to achieve efficiency by avoiding re-rendering of unnecessary code that can be memoized.

How does it work?

const cachedFn = useCallback(fn, dependencies)
Enter fullscreen mode Exit fullscreen mode

The useCallback hook takes two arguments: the function to memoize, and an array of dependencies. The dependencies are the values that the function depends on. If any of the dependencies change, the function will be re-run.

Why don't we just store the function outside the component?

We may be tempted to store the function as any other value stored in a variable (in JS, functions are first class functions).

import React, { useState } from 'react';

// Outside the component
function myFunction() {
  console.log('Function executed');
  return 'Function result';
}

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

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent func={myFunction} />
    </div>
  );
}

function ChildComponent({ func }) {
  const result = func();

  return <div>{result}</div>;
}

export default ParentComponent;
Enter fullscreen mode Exit fullscreen mode

In this example, even if you click the "Increment" button and cause the ParentComponent to re-render, the myFunction outside the component won't re-run unless explicitly called. However, if myFunction was used in the rendering logic of the ChildComponent, the child component might re-render if the parent component's re-render caused changes in the prop values.

So, every time the component re-renders, myFunction will be re-run, even if there is no conditional but simply some plain console logging and returning of a string. This can be inefficient, because myFunction may be expensive to run.

The useCallback hook goes a step further in limiting re-renders

The hook limits re-renders to only when one of the dependencies' value change:

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

// Inside component as a definiton, but actually inside React cache

function ParentComponent() {
  const [count, setCount] = useState(0);
  const myFunc = useCallback(()=>{
     function myFunction() {
        console.log('Function executed');
        return 'Function result';
     }
  },[])

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent func={myFunc} />
    </div>
  );
}

function ChildComponent({ func }) {
  const result = func();

  return <div>{result}</div>;
}

export default ParentComponent;
Enter fullscreen mode Exit fullscreen mode

In this example, the myFunction function will not be re-rendered even if the update of the ParentComponent's state triggers a re-render in the ChildComponent state via the props.

But where is the function stored?

When using the useCallback hook in React, the function is stored outside of the render cycle. The purpose of useCallback is to return a memoized version of the function, meaning that it retains the same memory reference across renders unless one of the variables in its dependency array changes.

So to understand the useCallback hook, you have to understand how exactly it limits re-renders. Thus, one must look into the dependency array.

With simple words, the dependency array plays the vital role to control when it is acceptable to allow re-renders. That would be if a variable that the function depends on, changes its value.

A more complex example would be an onCancel function. This function is part of a personal project of mine - Airbnb clone. It accepts an id of an item and updates the Front-End via update of state and the Back-End via an axios request.

const onCancel = useCallback((id: string) => {
    // del FE
    setDeletingId(id);
    // del BE
    axios.delete(`/api/reservations/${id}`)
        .then(() => {
            toast.success('Reservation cancelled');
            router.refresh();
        })
        .catch((error) => {
            toast.error(error?.response?.data?.error);
        });
}, [router]);
Enter fullscreen mode Exit fullscreen mode

So why do we put the router variable in the dependency array?

The router variable may change between the time we trigger the cancel function and the time the router.refresh() method is called. To ensure that the onCancel function is re-evaluated, we need to add the router variable to the dependency array of the useCallback hook.

The opposite is true for the setDeletingId. It does change a state if you look into the whole code, but this state does not change in any way the variables that the onCancel function depends on.

Therefore, the dependency array is a crucial instrument that must be configured so that the useCallback hook always works with up-to-date variables it may depend on. In a sense, it serves the opposite purpose of useCallback, since it allows for re-renders to happen, so that useCallback can have the most recent data it works with. But it also limits the re-renders up to the sanitary minimum to avoid any waste of resources that complex functions inside component may cause.

Top comments (0)