Why Effects shouldn't lie about their dependencies
Tags: react, hooks, javascript, webdev
Published: 2019-07-03 13:33:58

For a long time React developers have been inventing and sharing different patterns of reusing code logic — [Higher order component](https://reactjs.org/docs/higher-order-components.html) and [render props](https://reactjs.org/docs/render-props.html) are to name a few. It was because **React had no stateful primitive simpler than class component**. Finally, the advent of [hooks](https://reactjs.org/docs/hooks-intro.html) into the React ecosystem has uprooted this problem and made code reusability a breeze. If you are interested to know why hooks were introduced, I have a written a separate in-depth article about it [here](https://medium.com/@5066aman/how-to-write-90-cleaner-code-with-hooks-a3604116fb90?source=friends_link&sk=e3d0a1d284d45faa357638950421d520).

In this article, I am going to talk about *useEffect* hook only and will share some of my learnings and few caveats associated with them. A few things we will discuss are:

1. We will start off with an example of a using useEffect, which has a bug.
2. Then, we will try to demystify the cause of this bug 😀.
3. And finally, we’ll see how can we avoid these bugs and write effects which are easy to reason about.

Before we go ahead, I’d like you to unlearn what you have been doing with the class component’s lifecycles. With hooks, we need a different mindset.

> *In hooks land, functions are the kings.*

Enough of the background. Let’s get started now.

### A recap
Side effects are an indispensable part of any web applications. Fetching data, manually mutating DOM, and setting up subscription are all examples of side effects. The *useEffect* hook lets you perform side effects in your function component.

```jsx
 // Inside your function component 
 useEffect(() => {
 // some side effect code 
 });
}
```
I have seen some of the developers assuming that it’s always the same effect (anonymous function) which React calls after every render. But this is not the case. 
Every time a re-render happens, we schedule a **new effect** replacing the **previous effect**. This is intentional and important as it makes the effect behaves more like a part of the render result. The key point to remember here is that each effect “belongs” to a particular render.

> *Each effect “belongs” to a particular render.*

There is also an optional second argument to useEffect call — the dependency array. This is a mechanism by which React knows when to skip running your effect if certain values haven’t changed between re-renders.

In the cases where effects require cleanup, we can optionally return a cleanup function. Keep in mind that React always calls this cleanup function before applying the next effect.

> *React always clean up the previous effect before applying the next effect.*

With basics aside, let’s now move onto the fun part.

### 1. A buggy effect
Here is an example code snippet demonstrating the usage of setInterval (a side effect) inside useEffect hook:

```jsx
function CounterWithBug() {
  const [count, setCount] = useState(0);
useEffect(() => {
    const id = setInterval(() => setCount(count + 1), 1000);
    return () => clearInterval(id);
  }, []);
return <h1>Count is {count} </h1>;
}
```

Just by looking at this code, can you identify any bug?

This code may look perfectly fine but our count value doesn’t increment. Here’s the [demo](https://codesandbox.io/s/counterwithbug-sqn37) link if you wish to see that in action. You might be thinking that *setInterval* callback is calling the setter which should increment the count value after every 1 second. But this is not happening. What are we missing?

### 2. Demystifying the cause of the bug
We can definitely fix this issue with one small change and I’m sure most of you know how. But let’s take a step back and try to understand why this behaviour exists.

Every time when callback inside the *setInterval* calls the setter, React does a re-render. Doing so creates a new effect (function). But interestingly, as we have passed an empty *dependency array* [], which is a signal to React to skip applying this effect after the first render, it never gets invoked the second time. 
Now you might be wondering how does it make a difference: our setter is being called every time and so it should increment the *count* value. Right?

This behaviour has nothing to do with React. It’s about how [closures](https://medium.com/r/?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FClosures) work in JavaScript. In simple words, all functions in ECMAScript are closures since all of them at creation stage **lexically** captured the scope chain of itself and parent context. This is regardless of whether a function is **activated** later or not.

Let’s consider an example:

```js
let x = 10;
// function is created here (not invoked yet)
function bar() {
  console.log(x);
}
function foo() {
  let x = 50;
  bar(); // invocation happens here
}
foo(); // will print 10
```

When *foo* is invoked, *10* will be printed, but not *50*. This is because when the *bar* is created earlier (function creation stage), **x** is stored statically into its scope chain and that’s what gets resolved when bar execution is activated later on.

Let’s consider one more example to strengthen our closure concept.

```js
function parent() {
  let x = 20;
  setTimeout(() => console.log(x), 1000);
}
parent(); // prints 20 after a minimun time delay of 1 sec.
```
 
Even though the parent execution context is destroyed, callback inside the interval still manages to print the correct value of x after 1 second delay. This happens because of the **closure**. The inner function, statically at creation time, captures the variables defined in the parent scope.

> The inner function, statically at creation time, captures the variables defined in the parent scope.

If you want to delve more into the concept of closures, I’ve written an article about it [here](https://medium.com/@5066aman/lexical-environment-the-hidden-part-to-understand-closures-71d60efac0e0?source=friends_link&sk=7831bd115f13b3f667c877fb64aa4c9e) too.

Now taking this new knowledge along, let’s visit our effect one more time from a different angle. Here’s the snippet again so that you don’t have to scroll up:

```jsx
function CounterWithBug() {
  const [count, setCount] = useState(0);
useEffect(() => {
    const id = setInterval(() => setCount(count + 1), 1000);
    return () => clearInterval(id);
  }, []); // 🛑 missing the 'count' dependency
return <h1>Count is {count} </h1>;
}
```

When the effect is executed after the first render, the anonymous callback inside setInterval **statically** captures the *count* value from its parent context. This happens at the **creation stage** and the value captured is 0. After a minimum delay of 1 sec, this callback is invoked, which in turn calls the setter with a new value of 1 (0 + 1). In response to this, React re-renders the component and you get to see the new *count* value of 1 in the UI.

Now, as dependency array is empty, React will only create a new effect replacing the previous one, but **never** runs it. And as we just learned that React always cleans up the previous effects **before** applying the next effects, it will not bother running the cleanup in this case. Consequently, the initial interval never gets cleared out and our anonymous callback is still holding on to the count value of 0 into its scope chain. When the setter is called, the new value passed to it is always 1 (0 + 1). This is why the count value doesn’t increment beyond 1.

### 3. Never lie about your effect’s dependencies — a few fixes

After successfully unveiling the root cause of the bug, now is the time to fix it. It’s always easy to find a cure when you know the exact source of the problem. The problem was that the interval captured the count value of 0 statically when the first render happened. So, the solution is to make the interval captured the latest count value every render. How can we make that possible? Can we take help from React?

Yes! you guessed it right — *the dependency array*. Whenever the value inside the dependency array changes, React cleans up the previous effect and applies the new one.

#### Fix 1: using ‘count’ as a dependency
In our buggy code example, we just need to pass the **count** variable as a dependency to fix the issue. Here’s the [demo link](https://medium.com/r/?url=https%3A%2F%2Fcodesandbox.io%2Fs%2Fpeaceful-wave-9uudg).

```jsx
function Counter() {
  const [count, setCount] = useState(0);
useEffect(() => {
    const id = setInterval(() => setCount(count + 1), 1000);
    return () => clearInterval(id);
  }, [count]); // ✅ passing 'count' as dependency
  // will render the correct value of count
return <h1>Count is {count} </h1>;
}
```
Now with this little change, whenever the *count* value changes, React goes ahead and first call our cleanup mechanism which clears up the previous interval, and then sets a new interval by running the effect again. **Bingo!!** 🎉

In our code, the effect has a dependency over the *count* variable. So, it should also be inside the dependency array.

So, the lesson is here that an effect should always be honest about its dependency. Every time this promise fails, a buggy code behaviour may appear.

> Effect should never lie about its dependency.

#### Fix 2: completely removing the dependency array
Another fix to resolve this issue is to completely remove the dependency array. When there’s no dependency array, React will make sure to follow the routine of clearing up the previous effect before running the new one. And now, of course, you know why does it make a difference 😀

```jsx
function Counter() {
  const [count, setCount] = useState(0);
// the following effect will run after the first render and after each update
  useEffect(() => {
    const id = setInterval(() => setCount(count + 1), 1000);
    return () => clearInterval(id);
  }); // ✅ No dependency array here.
  // will render the correct value of count
return <h1>Count is {count} </h1>;
}
```
Here’s the [demo](https://medium.com/r/?url=https%3A%2F%2Fcodesandbox.io%2Fs%2Fcountewithoutdeps-0ptoo) in action.

#### Fix 3: using ‘updater’ function inside the setter
Now, if you have a sharp eye, you might have noticed that both aforementioned fixes are not very efficient. We are creating a new interval for each render. Our counter may run slowly as the browser has to clear up the previous interval before applying the new one. This could take some microseconds which could slowly add up and our counter would start to feel slow.

So, can we just set our interval once and only clear it when our component unmounts? The only way to do this is by passing an empty array. Right? But then we are again hitting the same issue as we saw above. We have to pass the *count* variable again.

Well, to solve this conundrum, we will follow the same rule of thumb — don’t lie about your effect’s dependency. Check out the [demo](https://medium.com/r/?url=https%3A%2F%2Fcodesandbox.io%2Fs%2Fcounterusingstateupdater-5ynwv) here.

```jsx
function Counter() {
  const [count, setCount] = useState(0);
useEffect(() => {
    // ✅ No more dependency on `count` variable outside
    const id = setInterval(() => setCount(c => c + 1), 1000);
    return () => clearInterval(id);
  }, []);
return <h1>Count is : {count}</h1>;
}
```
Here we are using the updater function inside our setter function which doesn’t depend on *count* variable outside. Doing so, allow us to use an **empty** dependency array. We are not lying to React about our effect’s dependency. This is a moment of proud 👏.

#### Fix 4: “useRef” to the rescue
Before wrapping it up, I want to show you one more solution to this issue. This solution is based around using another hook called [useRef](https://medium.com/r/?url=https%3A%2F%2Freactjs.org%2Fdocs%2Fhooks-reference.html%23useref).

I don’t want to go into much details of explaining how *useRef* works. But I think of them as a box where you can place any value. They are more like [instance properties](https://medium.com/r/?url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen-US%2Fdocs%2FWeb%2FJavaScript%2FReference%2FClasses%23Instance_properties) in JavaScript classes. The interesting fact is React **preserves** the value of a returned object from *useRef* across different renders.

Let’s visit our code example again the last time:

````jsx
function CounterUsingRef() {
  const [count, setCount] = useState(0);
// ✅ putting fresh count into the latestCount
  const latestCount = useRef();
useEffect(() => {
    // ✅ make sure current always point to fresh value of count
    latestCount.current = count;
  });
useEffect(() => {
    const id = setInterval(() => setCount(latestCount.current + 1), 1000);
    return () => clearInterval(id);
  }, []);
return <h3>Counter with useRef: {count}</h3>;
}
```
Again we’ve kept our promise of **not lying** about our dependency. Our effect is no more *count* variable dependent.

Even though the interval is still statically capturing the *latestCount* object (as it does in the case of the first buggy example), React makes sure the **mutable** *current* always gets the fresh count value. 🙂

Here’s the [demo](https://medium.com/r/?url=https%3A%2F%2Fcodesandbox.io%2Fs%2Fcounterusingref-ekdzd) for above code snippet if you’re interested.

### Conclusion
Let’s recap what we have just learned:
1. The **function** passed to *useEffect* is going to be **different** on every render and this behaviour is intentional. 
2. Every time we re-render, we schedule a **new** effect, replacing the **previous** one. 
3. All functions, at the **creation stage**, statically captures the variable defined in the **parent scope**. 
4. We should **never lie** to React about our effect’s dependencies.

---

I hope this article was interesting to read and has helped you understand why dependency array plays an important role in our effects. Consequently, I strongly recommend installing an ESLint plugin called [*eslint-plugin-react-hook*](http://ks%28https//www.npmjs.com/package/eslint-plugin-react-hooks) that enforces this rule.

Here’s a single [link](https://codesandbox.io/s/setintervalwithuseeffect-vbneb) of all the demos combined in one file. Keep an eye on the second fix and see how it is slower 🐢 than the last two fixes.

Also, Let me know your thoughts in the comments below and if you liked it, a few 👏 will definitely make me smile 😃. Now go ahead and share this knowledge with others.