DEV Community

Yiğit Kaan Korkmaz
Yiğit Kaan Korkmaz

Posted on

Avoid Stale Closures In React

Hello everyone! In today's post i will talk about stale closures, a subject that can affect your code in a bad way.

First of all, what is a closure, yet alone a stale one?

A closure in JavaScript is when an inner function encloses over the outer one and remembers variables in the outer function for later use. This is thanks to the lexical environment of JavaScript.

But what do i mean by that? Let's take a look at the example below:

const createMultiplier = (multiplyBy) => {
  const multiplier = (toMultiply) => {
    return multiplyBy * toMultiply;
  }

  return multiplier;
}

const double = createMultiplier(2);
const ten = double(5);
Enter fullscreen mode Exit fullscreen mode

In the example above we used closures to create a multiplier function. To explain further, the multiplier function enclosed over multiplyBy variable from it's outer scope, remembering it when the function will be called.

A stale closure is when the inner function remembers the outdated value of a function. An example can be:

let a = 0;
const add = () => {
  a += 1;
  const message = `Variable a is incremented to ${a}`;

  return () => {
    console.log(message);
  }
}

const log = add();
add();
add();

log(); // Outputs 1, Expected output: 3
Enter fullscreen mode Exit fullscreen mode

In the example above we create a number variable that starts with 0, then create an add function that adds 1 to it. But when we use the add function 3 times without logging the first one, and then when we log it, it logs 1 instead of 3. Why is that?

That is called a stale closure. The log function enclosed over an outdated version of the a variable, and it logged that one instead of the current one, as it should have.

How would we fix this?

let a = 0;
const add = () => {
  a += 1;

  return () => {
    const message = `Variable a is incremented to ${a}`;
    console.log(message);
  }
}

const log = add();
add();
add();

log(); // Outputs 3, Expected output: 3
Enter fullscreen mode Exit fullscreen mode

This way when we use the log function, when it's time to execute it will enclose over the current a variable and get the right value.

If you still need more information, i will post another blog about Closures In JavaScript that you can check out when i publish it.

Now, can closures affect our React code? Check the example below:

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

const Timer = () => {
  const [time, setTime] = useState(0);
  const [isCounting, setIsCounting] = useState(false);

  useEffect(() => {
    if(isCounting) {
      const id = setInterval(() => {
        setTime(time + 0.1)
      }, 100);

      return () => {
        clearInterval(id)
      }
    }
  }, [isCounting])

  return (
    <div>
      The time is: {time.toFixed(1)}
      <br />
      <br />
      <button onClick={() => setIsCounting(!isCounting)}>
        {isCounting ? "Stop timer" : "Start Timer"}
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The example above is a stale closure. Can you see why?

If your answer is the dependency array, it is correct! React hooks rely heavily on the concept of closures, and when the Timer is first mounted, the initial value of time is 0. Therefore the callback of the setInterval captures that one, and tries to update it again and again, leaving our timer with the value 0.1 all the time.

So how can we fix this? There are two solutions.

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

const Timer = () => {
  const [time, setTime] = useState(0);
  const [isCounting, setIsCounting] = useState(false);

  useEffect(() => {
    if(isCounting) {
      const id = setInterval(() => {
        setTime(time + 0.1)
      }, 100);

      return () => {
        clearInterval(id)
      }
    }
  }, [isCounting, time]) // Added time as a dependency

  return (
    <div>
      The time is: {time.toFixed(1)}
      <br />
      <br />
      <button onClick={() => setIsCounting(!isCounting)}>
        {isCounting ? "Stop timer" : "Start Timer"}
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

We add time to the dependency array so whenever the time changes, React updates the functions accordingly with the right value. However, there is a second fix.

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

const Timer = () => {
  const [time, setTime] = useState(0);
  const [isCounting, setIsCounting] = useState(false);

  useEffect(() => {
    if(isCounting) {
      const id = setInterval(() => {
        setTime(time => time + 0.1) // Give the setTime function a callback
      }, 100);

      return () => {
        clearInterval(id)
      }
    }
  }, [isCounting])

  return (
    <div>
      The time is: {time.toFixed(1)}
      <br />
      <br />
      <button onClick={() => setIsCounting(!isCounting)}>
        {isCounting ? "Stop timer" : "Start Timer"}
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Giving a callback to a set function from a useState hook, allows react to automatically update the state from the current one. And also you don't need to put it inside a dependency array, which can sometimes create confusion, and arguably looks cleaner.

CONCLUSION

Closures are an essential part of JavaScript, and we need to understand them way better so we can write better code. And of course, avoid stale closures.

And as always, please let me know if there are wrong information on this article, so i can correct it! I'm very excited to hear your feedback if it helped you out or not!

Top comments (0)