DEV Community

Cover image for React useCallback and JS function closure 🤔
Wesam Alqawasmeh
Wesam Alqawasmeh

Posted on • Edited on

React useCallback and JS function closure 🤔


As many of you know that useCallback is a built-in hook in React, usually used to scale and optimize large applications rendering performance by memoizing the functions to reduce recreating them in each re-render occurs. But you may not know how this hook actually works and how that is related to function closures, so this is what I'm gonna explain here!

⚠️ If you don't know why useCallback is used for, I recommend you to go through useCallback in React docs before you keep going in this blog.



useCallback takes two parameters, a callback function and
an array of dependencies. In the first render useCallback returns the passed function, and for subsequent renders it compares the old dependencies with the passed one using Object.is algorithm, If nothing has been changed in the dependencies will return the same old function, otherwise will return the passed function.

🔻See this simple skeleton for useCallback🔻

const memoizedCallback = useCallback(
  () => {
    // function logic goes here
  },
  [dependencyArray]
);
Enter fullscreen mode Exit fullscreen mode

🔻And this is a simulation for useCallback as a normal JS function🔻

/*
    Please note that this code is theoretical, this is not 
    how we actually compare two arrays together.
 */

let oldDependancies, oldCallBack; //React stores these somewhere

function useCallback(callback, dependencies) {
  if(oldDependancies === dependencies) return oldCallBack;

  oldDependancies = dependencies;
  oldCallBack = callback;

  return callback;
}
Enter fullscreen mode Exit fullscreen mode

Theoretically the above code compares the old dependencies with the passed one, if the old one is the same as the passed one then will return the old callback function, otherwise it will assign the new dependencies array and the new callback to the old one, and returns the passed function.

Now lets dive into with a real example, the great Counter component 🤯

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

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

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

  const decrementCount = useCallback(() => {
    setCount(count - 1);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
      <button onClick={decrementCount}>Decrement</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This component renders a count number with increment and decrement buttons. Each of increment and decrement handlers memoized (cached) using useCallback, and it's working as expected when click the buttons will increase and decrease the count by 1. You can interact with it using the below box!

Problem

Let's change the above code a bit by removing the count from the dependencies array in each useCallback for incrementCount and decrementCount, to be like this.

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

  const decrementCount = useCallback(() => {
    setCount(count - 1);
  }, []);
Enter fullscreen mode Exit fullscreen mode

Can you guess now what are the results when click increment and decrement buttons multiple times? check your answer using the below box

Now after you've clicked many times on the buttons, apparently the count won't increase more than 1 or decrease less than -1, and the reason behind that is the function closure.

Using useCallback with an empty dependency array tills react to create the incrementCount and decrementCount functions only once, and not to recreate them on every render. Therefore, the closure of these functions contains the initial (stale) value of count, which is 0.

When the incrementCount function is called, it sets the value of count to count + 1, but count is still the old value of 0 in this closure, so the result is 1. On subsequent calls to incrementCount, the closure still contains the old value of count, which is 0, so the result is still 1. And the same for decrementCount the result will allways be -1.

Solution

The solution for this issue is to add each variable affects the result to the dependency array, or by passing an updater function to setCount instead of count +- 1.

So incrementCount will be like this

const incrementCount = useCallback(() => {
    setCount(count + 1);
  }, [count]);
Enter fullscreen mode Exit fullscreen mode

OR

const incrementCount = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);
Enter fullscreen mode Exit fullscreen mode

The reason is that updater function works with an empty dependency array, is this updater function is called during the rendering process and reads the actual value of the state at that time, to know more check queueing a series of state updates.

Conclusion

  • Each variable affects the result of the function passed to useCallback should be added to the dependency array.
  • If the state change depends on the old state it's better pass an updater function to the state setter function.
  • The memoized callback will has the same old closure until re-creating it because of dependency changes.



Thanks for reading!

Let me know if you have any thoughts by commenting below.

Top comments (2)

Collapse
 
awnialrifai profile image
Awni-AlRifai

Very Informative Article!

Collapse
 
wesamalqawasmeh profile image
Wesam Alqawasmeh

Happy to hear that! Thanks!