Understanding the useCallback hook
Published: 2023-08-09 12:45:03


**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)
```

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;
```

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;
```

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](https://github.com/VasilGVasilev/airbnb/blob/main/airbnb/app/properties/PropertiesClient.tsx). It accepts an id of an item and updates the Front-End via update of state and the Back-End via an axios request.

```sh
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]);
```

**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.