React Effect Hook Explained
Tags: react, javascript, tutorial
Published: 2021-02-15 20:35:34

I've recently learned about React's inbuilt `useEffect` hook and let me tell you - it's neat! 💙 It's commonly used to retrieve data from external APIs and handle timers. This article will walk you through a step-by-step implementation of a timer component via the `useEffect` hook. 

Let's say our timer component should update each second, like so: 

![Timer working](https://dev-to-uploads.s3.amazonaws.com/i/sujc2do0pjeef432bk2w.gif)

If you're new to React, you might be tempted to define an interval at the top level of your component, as below: 

```js 
import React, { useState } from "react";
import "./TimerDemo.css";

const TimerDemo = () => {
  const [seconds, setSeconds] = useState(0);

 setInterval(() => {
    setSeconds((s) => s + 1);
  }, 1000);
 
  return (
    <div className="TimerDemo">
      <h1>Timer Demo</h1>
      <div>⌛ {seconds} ⌛</div>
    </div>
  );
};

export default TimerDemo;
```
However, the above code will result in the following output. 

![Timer going mad](https://dev-to-uploads.s3.amazonaws.com/i/nwslr03dhxkawm9qaar3.gif)

What's going on here? Has React broken the Universe and altered the rules of spacetime? 👽 Not quite. What's happening here is that multiple intervals are being set over and over in quick succession. 

When the component renders for the first time, the interval is set, which changes the state of `seconds` each second. Once the state changes, a re-render of the entire component is immediately triggered, and the code inside the component runs once more. Upon running, the code will result in another, identical, interval being set. But the old interval will also continue doing its thing and manipulating the state of `seconds`. 

So now we have two intervals that are both behaving the same way: updating state each second and triggering the component to re-render. Each re-render will lead to more fresh intervals being set, which in turn will trigger state change. This cycle - a positive feedback loop - will repeat ad infinitum (or more likely ad browser-crashium). 🔁

**The correct way to handle timers in React is by using the splendiferous `useEffect` hook.** The top 3 things to know about this hook are: 

* it accepts a _callback function_ as the 1st argument
* it accepts an _array_ as its optional 2nd argument
* it returns null, but can optionally be customised to return a _function_, which is useful for "cleanup" purposes
* it's hard to represent emojitively, because there isn't yet cross-browser support for the hook emoji, so here's an anchor instead ⚓ (okay, this one was a bonus)

We'll examine how each of these points relates to handling timers in React. (Except the emoji one. Which doesn't.) 

### 1. `useEffect` Callback Function

The first argument the hook accepts is a callback function. This function is what React understands to be the "effect". In this case, the effect is our interval. Let's define it inside a `useEffect` hook. 

```js
const TimerDemo = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  });

  return (
    <div className="TimerDemo">
      <h1>Timer Demo</h1>
      <div>⌛ {seconds} ⌛</div>
    </div>
  );
};
```
This will display as the following: 

![Timer still going mad](https://dev-to-uploads.s3.amazonaws.com/i/nft8yusmarh7k1ftuaf4.gif)

Not at all what we want, our issue remains. This is where the second argument of the `useEffect` hook comes into play. 

### `useEffect` Dependency Array

The second argument is an optional array of state or prop values, which specify when the effect should run. We have 3 options here: 

1. No array: if the array is left out, as in the previous code snippet, the effect will run after _every_ render.
2. Empty array: the effect runs _once_ after the initial render. 
3. Array with state or prop values: the effect runs _only_ when any of these values change. 

In our case, if we passed in `[seconds]`, we would tell React to re-run the effect each time the state of `seconds` changes. Which would, of course, be completely pointless - in fact, this is exactly what we're trying to avoid. 

And avoid it we can; specifically, by passing in an empty dependency array, `[]`. An empty array tells React to only run the effect once, after the component renders for the first time. Let's examine the code below: 

```js
const TimerDemo = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  }, []);

  return (
    <div className="TimerDemo">
      <h1>Timer Demo</h1>
      <div>⌛ {seconds} ⌛</div>
    </div>
  );
};
```
![Timer working](https://dev-to-uploads.s3.amazonaws.com/i/sujc2do0pjeef432bk2w.gif)

Voila! The timer works now. 

### useEffect Return Function

But what would happen to the interval if we removed the timer component from the DOM? Let's create a scenario where it is removed by wrapping it in another component, Clock, which toggles the timer component. Let's also include a toggle button that removes or adds the timer component to the DOM. 

```js
const Clock = () => {
  const [isHidden, setIsHidden] = useState(false);
  const toggle = () => setIsHidden((hidden) => !hidden);

  return (
    <div className="Clock">
      {!isHidden && <TimerDemo />}
      <button class="Clock-btn" onClick={toggle}>
        Toggle
      </button>
    </div>
  );
};
```

![Timer seemingly working as intended](https://dev-to-uploads.s3.amazonaws.com/i/72gr7cv0vo6127twhziz.gif)

At first glance, removing the TimerDemo component from the DOM appears to work as intended. But upon opening the console, an angry sea of red appears. React is not happy. 😡

![Error message](https://dev-to-uploads.s3.amazonaws.com/i/dsv3pvy5rknl2j34uxaj.png)

Why is this? Well, when the component is removed, despite disappearing visually, the interval associated with it simply keeps going. There's nothing telling it to stop executing. The interval will proceed to try and update the state of a currently unmounted component, greatly upsetting React in the process. Poor React! 🥺

So, how do we tell the interval to stop when the component is removed from the DOM? By specifying `useEffect`'s return value. By default, it returns null, but we can modify this to return a callback function, which will act as "cleanup". Warning: the cleanup function can feel a bit abstract to read about, and the best way to befriend it is to use it and explore its functionality first-hand. 

The cleanup is executed at the following times: 

* After the initial rendering, the hook _only_ invokes the effect. The cleanup function _doesn't_ run
* On all following re-renderings, the cleanup from the previous effect execution is invoked _first_, after which the current effect runs
* The cleanup also runs _after_ the component is unmounted, i.e. removed from the DOM

Let's define our cleanup function. In order to stop an interval, we need to capture the interval ID and pass it into a `clearInterval` function (a vanilla JS concept). We'll return this function inside our `useEffect` and add some print statements to monitor the sequence of execution. 

```js 
  useEffect(() => {
    console.log("I am the effect. PARTY! 🎺 👐 🎉");
    const timerId = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);

    return () => {
      console.log("Cleaning up... 🙄 🧹🧼");
      clearInterval(timerId);
    };
  }, []);
```
![cleanup function demonstration](https://dev-to-uploads.s3.amazonaws.com/i/rxoarrw7bkrw3blcbvov.gif)

In this case, the effect runs when the component is mounted, but never again, due to our 2nd argument, the dependency array, being empty. Therefore, the cleanup function will only run when the component is unmounted, thereby clearing the interval and preventing the error message. The toggle button now works as intended, and React is so happy it can barely contain its excitement. 


**I hope you are too, after making it through this tutorial! 👏**