Frontend Dad Blog.

Closing Time: Stale Closures in React and Javascript

Cover Image for Closing Time: Stale Closures in React and Javascript

Recently, a member of my team with a fair amount of React experience ran into a bug. Their component was referencing a state variable in its markup, but the value wasn't updating as expected. It was stale. This was an example of the "Stale Closure" phenomenon that can affect all Javascript applications, but tends to be extra sneaky in React. It's a heady concept and so I will almost certainly confuse you even more by attempting to explain!

*Note: I will provide code examples in plain Javascript for clarity/simplicity

Background: Scope in Javascript

Scope works in interesting ways in Javascript. Of particular note is the concept of "Lexical Scope". Simply put, lexical scoping explains the process by which a function's inner functions can reference values above or outside that function's definition. Consider the below example:

function scopeDemo() {
  const value = 2;

  function closure() {
    console.log(`value is ${value}`);
  }

  return closure;
}

const demo = scopeDemo();

demo(); // logs "value is 2"

In the code above, we define a function, scopeDemo, that contains a value const and a closure function. The closure function is able to "look outside itself" to see what value is. Note that I am literally calling the function closure for the sake of explanation. This would work regardless, and closure is not a reserved name in Javascript (though maybe it should be).

Changing values

Now let's expand the above example just slightly. I'll make the value mutable, and add a function that can update it.

function scopeDemo() {
  let value = 2;

  function closure() {
    console.log(`value is ${value}`);
  }

  function tick() {
    value += 1;
  }

  return [tick, closure];
}

const [tick, closure] = scopeDemo();

tick()
tick()
tick()
closure() // "value is 5"

This works as expected. This function returns an array of functions that operate on it. A few years ago, this style of returning an array from a function in Javascript would've looked a bit alien, more Python-esque. React's hooks have normalized this, but with a few caveats as we will see. Regardless, this works (for now).

Stale Closures: Frozen in Time...

OK, let's ruin everything and confuse ourselves by introducing some code that will result in our closure being "stale".

function scopeDemo() {
  let value = 2;

  const stale = `Hey the value is still ${value}!  I'm stale!`

  function closure() {
    console.log(stale);
  }

  function tick() {
    value += 1;
  }

  return [closure, tick];
}

const [closure, tick] = scopeDemo();

tick();
tick();
tick();
tick();
closure() // "Hey the value is still 2!  I'm stale!"

Uh oh. Something is off here. Let's try ticking a few more times...

tick();
tick();
tick();
closure() // "Hey the value is still 2!  I'm stale!"
tick();
tick();
tick();
closure() // "Hey the value is still 2!  I'm stale!"

Well this isn't right. What is happening here?

In the example above, our closure has become "stale". It's referencing an outdated value. We have been incrementing the value by calling tick repeatedly... but no luck. Why is this happening?

The answer lies with how functions in Javascript "capture" their lexical environment when they "close" over certain values.

When Javascript functions are "created", they retain full access to their lexical environment, and the Javascript runtime will make sure that environment is persisted in memory until it's no longer needed.

But how come this worked an example back, before the introduction of the stupid stale variable? We never recalculated the value of stale. In this example, Javascript referenced a value, stale at the time of the closure function's "creation".

Let's look at a slightly less contrived example:

function fetchData() {
  let data = null;

  // Fake API Call 
  setTimeout(() => {
    console.log("fetching and updating data!")
    data = 'some data';
  }, 1000);

  function getData() {
    console.log(data); // Captures `data` from the outer scope, creating a closure
  }

  return getData;
}

const getData = fetchData();
getData(); // logs null

Stale closure bugs tend to pop up in the world of async Javascript. In the above example, the getData function has lexical access to the data value, but when we call the function, it is stale. This makes sense if we think about what the Javascript runtime is doing here. It's behaving exactly as instructed, but the stale data value might throw off a programmer expecting that value to stay "up to date".

Bugs like these are best avoided by leveraging async APIs within Javascript to ensure that a variable referenced from a closure has been updated before said function is called:

function fetchDataAsync() {
  let data = null;

  // Fake API Call 
  function fetch() {
    return new Promise((res, rej) => {
      setTimeout(() => {
        data = "Api Response";
        res();
      }, 1000);
    });
  }

  async function getData() {
    const resp = await fetch()
    console.log("data", data)
  }

  return getData;
}

const getData = fetchDataAsync();
getData(); // logs "Api Response"

Kind of a wacky example, but most state management libraries do something similiar under the hood, only in a much more elegant way.

Closures in React

As I mentioned initially, a colleague ran into a bug involving a stale closure in React. This is a common thing. I will include a simplified and slightly contrived example below:

export default function App() {
  const [myCoolValueFromRedux, setMyCoolValueFromRedux] = useState(0);
  const [isModalOpen, setIsModalOpen] = useState(false);
  console.log("rendering - what is the value from redux", myCoolValueFromRedux);
  useEffect(() => {
    window.addEventListener("resize", handleResize);
  }, []);

  useEffect(() => {
    setTimeout(() => {
      setMyCoolValueFromRedux(5);
    }, 1000);
  }, []);

  function handleResize() {
    console.log(
      "Resizing -what is the value from redux?",
      myCoolValueFromRedux
    );
    if (myCoolValueFromRedux > 1) {
      console.log("closing the modal!");
      setIsModalOpen(true);
    }
  }

  return <div>{isModalOpen && <h2>Modal is open </h2>}</div>;
}

The app was intended to open a modal on resize if a given value from the Redux store (crucially - dependent on an async network request) existed. But it wasn't working.

If you try running the code above in a sandbox environment, you will see that the top level render function recognizes the updated value coming down from the fake Redux store. Yet the resize handling function does not. It's a stale closure. The reason for the bug is that the handler was created with a reference to a value that was never updated. It would never be able to "see" the updated value.

Fixing stale closures in React

React includes a few ways to "fix" these issues. I include parenthesis around "fix" because closures are most definitely a feature of Javascript, not a bug. However, a lack of clarity around their use in React definitely leads to bugs.

The fix here lies in the dependency array provided to the useEffect hook. This array allows users to declare what values to "watch" for changes. If any value in the array changes during a render cycle, React will reexecute the logic within the hook. This includes recreating functions and thus eliminating stale closures. See the below code:

export default function App() {
  const [myCoolValueFromRedux, setMyCoolValueFromRedux] = useState(0);
  const [isModalOpen, setIsModalOpen] = useState(false);
  console.log("rendering - what is the value from redux", myCoolValueFromRedux);
  useEffect(() => {
    window.addEventListener("resize", handleResize);
  }, [myCoolValueFromRedux]); // added the value here to "watch"

  useEffect(() => {
    setTimeout(() => {
      setMyCoolValueFromRedux(5);
    }, 1000);
  }, []);

  function handleResize() {
    console.log(
      "Resizing -what is the value from redux?",
      myCoolValueFromRedux
    );
    if (myCoolValueFromRedux > 1) {
      console.log("closing the modal!");
      setIsModalOpen(true);
    }
  }

  return <div>{isModalOpen && <h2>Modal is open </h2>}</div>;
}

I focus on the pitfalls of useEffect in my example because it's been responsible for about 99% of closure related bugs I've seen in React.

Conclusion(s)

Closures are an integral aspect of Javascript, but they can be tricky. Understanding their underlying behavior is critical to hunting down bugs that might be a side effect of their use. When in doubt, check your effect hooks. Or pass a function to update your state.

Sources

MDN