React Hooks Dependencies and Stale Closures
Tags: react, beginners, javascript, hooks
Published: 2021-07-10 13:10:19

After we got the confidence on the [flow of hooks](https://dev.to/payapula/react-useeffect-hook-flow-2f7p) in React, it's important to understand about it's dependencies as well. 

In this post we will dive a bit deeper into the dependency array of hooks.

As always, let's start with a Javascript example. Before looking at the output, try to guess what would be logged.

```javascript
function App(count) {
  console.log('Counter initialized with ' + count);
  return function print() {
    console.log(++count);
  };
}

let print = App(1);
print();
print();
print();

print = App(5);
print();
print();
```

The above function is a simple example of **closure** in JavaScript. The console output is as below.

```
Counter initialized with 1 
2
3
4
Counter initialized with 5 
6
7
```

If you can get it, then great! I will go ahead and explain what is happening.

The `App` function returns another function called `print` this makes our `App`, a higher order function. 

> Any function that returns another function or that takes a function as argument is called as Higher order function. 

```javascript
function App(count) {
  console.log('Counter initialized with ' + count);
  return function print() {
    console.log(++count);
  };
}
```

The retuned function `print` _closes over_ the variable `count` which is from its outer scope. This closing is referred to as **closure**. 

Please don't get confused with the name of the functions. Names need not necessarily be identical, as for an example

```javascript
function App(count) {
  console.log('Counter initialized with ' + count);
  return function increment() {
    console.log(++count);
  };
}

let someRandomName = App(1);
someRandomName(); //logs 2
```

Here App is returning a function `increment` and we are assigning it to the variable `someRandomName`

To define a "Closure",

> A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. ~ [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures)

Ah? that doesn't look like simple definition right ?

Alright, MDN is not much helpful here let us see what W3Schools says 

> A closure is a function having access to the parent scope, even after the parent function has closed. ~ [W3Schools](https://www.w3schools.com/js/js_function_closures.asp)

When we call the `App` function, we get the `print` function in return.

```javascript
let print = App(1);
```

The `App` function gets count as 1 and returns `print` which simply increases the count and logs it. So each time when `print` is called, the count is incremented and printed.

_If we are writing logic that uses closures and not careful enough, then we may fall into a pitfall called...._ 

## Stale Closures

To understand what is stale closures, let us take our same example and modify it further. 

Take a look at this code and guess what would be getting logged into the console.

```javascript
function App() {

  let count = 0;

  function increment() {
    count = count + 1;
  }

  let message = `Count is ${count}`;
  
  function log() {
    console.log(message);
  }

  return [increment, log];
}

let [increment, log] = App();
increment();
increment();
increment();
log();
```

To break it down,

1. There are two variables `count` and `message` in our App.
2. We are returning two functions `increment` and `log`. 
3. As per the name, `increment` increases our `count` and `log` simply logs the `message`. 

Try to guess the output. Let me give you some space to think.
.
.
.
.
.
.
.
.
Warning! 🚨 Spoilers 🚨 ahead
.
.
.
.
.
.
.
.

The output is

```bash
Count is 0
```

Oh, did we fail to increment the count?

Let's find out by placing console log inside our `increment` function 

```javascript
function App() {

  let count = 0;

  function increment() {
    count = count + 1;
    console.log(count);
  }

  let message = `Count is ${count}`;
  
  function log() {
    console.log(message);
  }

  return [increment, log];
}

let [increment, log] = App();
increment();
increment();
increment();
log();
```

And this time, the output will be 

```bash
1
2
3
Count is 0
```

Yes, we are incrementing the `count` that is present in the lexical scope of `increment`. However, the problem is with the `message` and `log`.

Our `log` function _captured_ the `message` variable and kept it. So, when we increment the count, the `message` is not updated and our `log` returns the message "Count is 0".

To fix this stale closure, we can move the message inside of `log`

```javascript
function App() {

  let count = 0;

  function increment() {
    count = count + 1;
    console.log(count);
  }

  function log() {
    let message = `Count is ${count}`;
    console.log(message);
  }

  return [increment, log];
}

let [increment, log] = App();
increment();
increment();
increment();
log();
```

And executing would produce the result,

```
1
2
3
Count is 3 
```

As per the name, _stale closure_ is when we fail to capture updated value from the outer scope, and getting the _staled_ value.

Hmm.. So, what does this _stale closure_ has to do in React?

## Hooks are nothing but Closures!

Let us bring the same JS example we saw above, into the react world, 

```jsx
function App() {
  const [count, setCount] = React.useState(0);

  let message = `Count is ${count}`;

  React.useEffect(() => {
    if (count === 3) {
      console.log(message);
    }
  }, []);

  return (
    <div className="App">
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount((c) => c + 1);
        }}
      >
        Increment
      </button>
    </div>
  );
}
```

After hitting `Increment` button three times, we should have a log that says "Count is 3".

Sadly we don't event get anything logged !!!

This is however not exact replica of our example from our JS world, the key difference is in our React world, `message` does get updated, but our `useEffect` just failed to capture the updated message. 

To fix this stale closure problem, we need to specify both `count` and `message` as our dependency array.

```jsx
function App() {
  const [count, setCount] = React.useState(0);

  let message = `Count is ${count}`;

  React.useEffect(() => {
    if (count === 3) {
      console.log(message);
    }
  }, [count, message]);

  return (
    <div className="App">
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount((c) => c + 1);
        }}
      >
        Increment
      </button>
    </div>
  );
}
```

Note - This is just a contrived example, You may choose to ignore either of those dependencies as both are related. If `count` is updated, `message` does get updated, so specifying just either of those is fine to get the expected output. 

Things are simple with our example, The logic that we wrote inside the hook is not really a side effect, but it will get more and more complicated if we start to write hooks for data fetching logic and other **real side effects**

The one thing that, always we need to make sure is,

> All of our dependencies for hooks must be specified in the dependency array and, we should not [lie to React about dependencies](https://overreacted.io/a-complete-guide-to-useeffect/#dont-lie-to-react-about-dependencies)

As I said, things get really complicated with closures in real-world applications and it is so very easy to miss a dependency in our hooks. 

From my experience, if we failed to specify a dependency and if not caught during the testing, later it would eventually cause a bug and in order to fix it we may need to **re-write the entire logic** from scratch !!

This is a big 🚫 NO 🚫 and **MUST BE AVOIDED** at all cost. But how?

## ESLint Plugin React Hooks

In order to make our life simpler, the react team wrote an ESLint Plugin called `eslint-plugin-react-hooks` to capture all possible errors with the usage of hooks.

So when you are all set up with this [eslint plugin react hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) When you miss a dependency, it would warn you about the possible consequence.

If you are using latest create-react-app then this comes out of the box (`react-scripts` >= 3.0)

As seen below, when we violate the [rules of hooks](https://reactjs.org/docs/hooks-rules.html) we will get a nice warning suggesting that we are probably doing something wrong.

![ESLint Warning on usage of hooks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/us4u54h4judkhmckjz9w.png)

The above image shows the error from ESLint that reads, _React Hook React.useEffect has missing dependencies: 'count' and 'message'. Either include them or remove the dependency array._

It even fixes the dependency problem with just a single click!

Keep in mind that a stale closure problem does not only affect `useEffect`, we would run into the same problem with other hooks as well like `useMemo` and `useCallback`.

The Eslint plugin works with all the React hooks, can also be configured to run on custom hooks. Apart from just alerting with dependency issues, it would also check for all the rules of hooks, So, make good use of it! 

Again to enforce,

> 🚫 Don't Lie to React about Dependencies and 🚫 Don't disable this ESLint rule 🤷🏾‍♂️


---

Big Thanks to:

- [Getting Closure on Hooks by Shawn Wang](https://www.swyx.io/getting-closure-on-hooks/)
- [Be Aware of Stale Closures when Using React Hooks](https://dmitripavlutin.com/react-hooks-stale-closures/)
- [A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/)
- [5 Tips to Help You Avoid React Hooks Pitfalls](https://kentcdodds.com/blog/react-hooks-pitfalls)
- [Epic React by Kent.C.Dodds](https://epicreact.dev/)