DEV Community

Cover image for When to use useCallback?
Romain Trotard
Romain Trotard

Posted on • Edited on • Originally published at romaintrotard.com

When to use useCallback?

The usage of useCallback is something very controversial where it exists two groups of person:

  • those who memoize everything
  • those who only memoize the strict necessary

In this article I expose you my rules which makes me decide to use useCallback.


What is useCallback

useCallback is a native hook provided by React, it permits to give you a memoized callback.

Why is it necessary?

As a quick reminder when developing with functional component, the body of the function is the render.

So if I define a function inside the component (render), this function will be redefined at every renders giving you a new references.

function myComponent() {
  // At each renders I am redefined
  // I.E. I will have a new references
  const onClick = () => {
    console.log("I have been clicked");
  };

  return <button onClick={onClick}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

Is it a problem to redefines function?

My answer is simply NO.

Most of the time we don't care. This is not a problem for our javascript engine to do it, it's fast and no memory issue with that.

So when do we care?
Let me just a second I want you to show a quick implementation of the hook before :)

A simplified implementation

The logic is pretty simple when you know how to implemt some memoization in JS. If it's not the case you can read my article :)

But in the case of React there is no closure.

Th previous callback and dependencies are stored in the Fiber node of the component. This is stored within the key memoizedState.

In the next code template, I show you an implementation example:

import shallowEqual from "./shallowEqual";

// Things stored in the React element
const memoizedState = {
  lastCallback: undefined,
  lastDependencies: undefined,
};

// In reality there is multiple implementation of 
// it behind the hood
// But it's a simplified example
function useCallback(callback, dependencies) {
  if (
    !shallowEqual(
      memoizedState.lastDependencies,
      dependencies
    )
  ) {
    memoizedState.lastCallback = callback;
    memoizedState.lastDependencies = dependencies;
  }

  return memoizedState.lastCallback;
}
Enter fullscreen mode Exit fullscreen mode

As you can see a shallowEqual is used to compare the dependencies. If you want to know more about the different types of equality, do not hesitate to read my article about it.

Warning: In reality React has multiple implementation of the hooks in function of the lifecycle phase: mount, update, ...

And now let's see with a quick gif how to see this in a browser:

memoizedState in the browser dev tool

Note: In a next article I will try to explain more about the structure of a Fiber node but it takes me some time to make a nice post about it :)

Note: Yep, it's memoized inside an array, but I put the data inside an object for simplicity.


Reasons for me to use useCallback

Performances issues

As usual, I will begin by told not to do premature optimization. Only do this when you have real performance problem in your application / component library.

For example if you have a component in your code base which has slow renders and that most of the time they can be prevent because it doesn't need to be re-render (no props change in reality).

In this case we will memo the component. And from here it's important that references do not change unnecessarily.

Now imagine that this component is a Button. Yeah it would probably not happen for a button, I know. But it's just an example ;)

So in this case it will be important that the onClick callback has a stable reference.

import React, { useCallback } from "react";

function App() {
  const onClick = useCallback(() => {
    // Doing some stuff here
  }, []);

  return (
    <MemoizedButton onClick={onClick}>
      Click me
    </MemoizedButton>
  );
}

function Button({ onClick }, children) {
  // Doing some heavy process here

  return <button onClick={onClick}>{children}</button>;
}

const MemoizedButton = React.memo(Button);
Enter fullscreen mode Exit fullscreen mode

Warning: If you do not put the useCallback, then the React.memo will be totally useless.

And the reciprocal is also true. If you useCallback but do not React.memo the Button then instead you make your performance worse.

Why?
Because as we have seen at each render there is 2 callbacks that are in memory.
Yep it's not dramatic, but by doing this, I find the codebase less readable.


When putting it as a dependency

Another reason which makes me useCallback is when I need to put the callback in the dependency of useEffect, useLayoutEffect or useCallback.

import { useCallback, useEffect, useState } from "react";
import apiCall from "./apiCall";

function App() {
  const [data, setData] = useState();

  const fetchData = useCallback(() => {
    apiCall().then(setData);
  }, []);

  useEffect(() => {
    // We fetch the data at mounting
    fetchData();
  }, [fetchData]);

  return (
    <div>
      <p>The data is: {data}</p>
      <button onClick={fetchData}>Refetch data</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: As you can see in the example I use the callback in the useEffect and in an event handler.

If it was used only in the useEffect, I would have defined the method directly in it:

useEffect(() => {
  const fetchData = () => {
    apiCall().then(setData);
  };

  // We only fetch the data at mounting
  fetchData();
}, [fetchData]);
Enter fullscreen mode Exit fullscreen mode

When doing public or generic hook

Another will be when I do some "public" hook, for example in a library, or a generic hook that could be used in multiple place.
Then I will stabilize returned callbacks.

Why do I do this?

The reason is that I don't know where it will be used. It could be:

  • in a useEffect/useCallback/useLayoutEffect then it will be required to have a stable reference
  • in an event handler, then it's not required at all

So to satisfy both cases, I provide a stable reference :)

import { useCallback } from "react";

export function usePublicHook() {
  return useCallback(() => {
    console.log("It's only an example");
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

But if I do a hook just to extract a specific logic from a component (for testing purpose and to make the component easier), and it can't be used in another one. Then I will only useCallback when it's necessary because I know the use case.


And here we go. That's how I use the hook useCallback, hoping that it can help you to have a better code base, because it makes the code more complicated to read.

To summarize:

  • if I have performances issues
  • if I used it as dependency of another hook (useEffect, useLayoutEffect, useCallback, ...)
  • when I do a public / generic hook

I hope to see React Forget released as soon as possible (yep I'm dreaming), which will help us to stop wondering :) If you don't know what is React Forget, let's check this video.

Do you use useCallback in an other use case? If it's the case do not hesitate to put it in comment.


Do not hesitate to comment and if you want to see more, you can follow me on Twitter or go to my Website.

Top comments (0)