The UseCallback Hook

The Basics

useCallback(fn, dependencies)

Right. Let's do this.

React does too much. Every time your component re-renders, everything inside of it (including your functions) are re-created.

Even if your function hasn't changed, React sees a "new" version on every render.

Usually, this isn't an issue, but it becomes an issue when you're passing the function down to a child that's trying to maintain efficiency, or returning the function from a hook that's dependent on state.

The problem: Without useCallback, a child component might re-render unnecessarily because it thinks it received a "brand new" function from the parent, even if nothing actually changed.

The solution: useCallback Ensures the function maintains the same "identity" across renders.

What's the difference between useCallback and useMemo?

useMemo Memoises (remembers) the result or the value returned by a function.

function AdditionComponent(firstNumber, secondNumber) {
    const additionResult = useMemo(() => {
        return firstNumber + secondNumber;
    }, [firstNumber, secondNumber]);
}
                        

In this case, useMemo will cache the value returned by the additionResult function. So if firstNumber = 1 and secondNumber = 2, the cached result will be 3.

useCallback remembers (caches) the function itself.

function AdditionComponent(firstNumber, secondNumber) {
    const handleAddition = useCallback(() => {
        return firstNumber + secondNumber;
    }, [firstNumber, secondNumber]);
}
                        

In this case, useCallback caches the function itself, and will only re-create the function if either of its dependencies (firstNumber or secondNumber) change.

Note: useCallback is essentially just a specific implementation of useMemo, designed only to store functions. Caching a value? Use useMemo. Caching a function? Use useCallback.

A real-world example

Imagine we want to create a custom hook called useArray, which returns a bunch of methods for performing operations on a given array:

export default function useArray(defaultValue) {
  const [array, setArray] = useState(defaultValue);

  const push = (newItem) => setArray((previous) => [...previous, newItem]);
  const filter = (callback) => setArray((previous) => previous.filter(callback));

  return {
    array,
    set: setArray,
    push, filter
  };
}

The problem is, any time our array state changes, all of those functions will need to be recreated and re-allocated in memory, seems like a bit of a waste, right?

This is where useCallback comes in. We can use it to cache the function definitions across re-renders, like this:

export default function useArray(defaultValue) {
  const [array, setArray] = useState(defaultValue);

  const push = useCallback(
    (newItem) => setArray((previous) => [...previous, newItem]),
    [],
  );
  const filter = useCallback(
    (callback) => setArray((previous) => previous.filter(callback)),
    [],
  );

  return {
    array,
    set: setArray,
    push,
    filter,
  };
}

Because we're wrapping the functions in useCallback with an empty dependency array, we're telling React to preserve the function references across re-renders. Even though JavaScript still instantiates the arrow functions on every render, useCallback checks whether or not the dependencies have changed, and disregards the new functions if they haven't, keeping the original (old) references in-place. This guarantees it will return the exact same memory reference, preventing unnecessary downstream updates.

With Typescript

Great news! You don't need to explicitly type this.

useCallback also uses type inference (noticing a pattern, yet?).