React.useEffect hook explained in depth on a simple example
Tags: javascript, webdev, react
Published: 2020-08-08 11:43:53

`useEffect` hook is an extremely powerful an versatile tool, allowing you to even create your own, custom hooks.

But using it involves surprising amounts of subtlety, so in this article we will see step by step how exactly this hook works.

In order to not lose focus, we will be using the most basic example imaginable and at each step we will control what is happening, by logging messages to the browser console.

You are highly encouraged to follow along this article and code all the examples yourself, using for example an online React repl like [this one](https://repl.it/languages/reactjs).

Let's get started!

## Basic use & behavior

`useEffect` is - as the name suggests - a hook to perform arbitrary side effects during a life of a component.

It is basically a hook replacement for the "old-school" lifecycle methods `componentDidMount`, `componentDidUpdate` and `componentWillUnmount`.

It allows you to execute lifecycle tasks without a need for a class component. So you can now make side effects inside a functional component. This 

was not possible before, because creating side effects directly in a `render` method (or a body of a functional component) is strictly prohibited. Mainly because we don't really control (and shouldn't really think about) how many times `render` function will be called. 

This unpredictability issue is fixed with the use of `useEffect`.

So let's create a simple functional component, which we will call `Example`:

```jsx
const Example = () => {
    return <div />;
};
```

It doesn't really do anything interesting, because we want to keep it as simple as possible, for the purposes of the explanation.

Note that we didn't use the shortened arrow syntax, where we can simply provide a returned value of a function (in that case a `div` element) in place of the body of the function. That's because we already know we will be adding some side effects in that body.

Let's do just that. 

I mentioned previously that it is prohibited to make side effects *directly* in the body of the component. That's where the `useEffect` hook comes in:

```jsx
import { useEffect } from 'react';

const Example = () => {
    useEffect(() => {
        console.log('render');
    });

    return <div />;
};
```

As you can see, we used `useEffect` function, which accepts a callback function as an argument. Inside the callback we just made a simple `console.log`, which will help us find out when this effect is executed.

If you render that component and look into a browser console, you will see `render` logged there once.

Okay. So we know that the callback is for sure called when the component first gets created and rendered. But is that all? 

In order to find out, we need to make a bit more involved example, that will allow us to rerender the `Example` component on command:

```jsx
import { useState } from 'react';

const Wrapper = () => {
    const [count, setCount] = useState(0);
    const updateCount = () => setCount(count + 1);

    return (
        <div>
            <button onClick={updateCount}>{count}</button>
            <Example />
        </div>
};
```

We created a new component called `Wrapper`. It renders both our previous component, `Example`, and a button. The button displays a counter value, initially set at `0`. After the button is clicked, the counter increases by one.

But the counter itself doesn't really interests us. we just used it as a trick to cause a rerender of the `Example` component. Whenever you click on the counter button, state of `Wrapper` component gets updated. This causes a rerender of the `Wrapper`, which in turn causes a rerender of the `Example` component.

So basically you are causing a rerender of the `Example` on each click of the button.

Let's now click few times on the button and see what is happening in the console.

It turns out that after each click, the `render` string again appears in the console. So if you click at the button 4 times, you will see 5 `render` strings in the console: one from initial render and one from the rerenders that you caused by clicking on the button.

Ok, so this means that a callback to `useEffect` is called on initial render *and* every rerender of the component.

Does it get called also when component gets unmounted and disappears from the view? In order to check that, we need to modify the `Wrapper` component once more:

```jsx
const Wrapper = () => {
    // everything here stays the same as before

    return (
        <div>
            <button onClick={updateCount}>{count}</button>
            {count < 5 && <Example />}
        </div>
};
```

Now we are rendering `Example` conditionally, only if `count` is smaller than 5. It means that when the counter hits 5, our component will disappear from the view and React mechanism will trigger it's unmounting phase.

It now turns out that if you click on the counter button 5 times, the `render` string *will not* appear in the console the last time. This means it will appear only once on initial render and 4 times on rerenders on the component, but not on the 5th click, when the component disappears from the view.

So we learned that unmounting the component does not trigger the callback. 

Then how do you create a code that is an equivalent of the `componentWillUnmount` lifecycle method? Let's see.

```jsx
const Example = () => {
    useEffect(() => {
        console.log('render');
        return () => {
            console.log('unmount');
        };
    });

    return <div />;
};
```

If your head spins from all the callbacks, that's fine - mine does to. But note that we didn't do anything too crazy. The callback passed to the `useEffect` function now returns an another function. You can think of that returned function as a cleanup function.

And here awaits us a surprise. We expected this cleanup function to run only on unmount of the component, that is when counter on our button goes from 4 to 5. 

Yet that is not what happens. If you run this example in the console, you will see that string `unmount` appears in the console at the end when component is unmounted, *but also* when the component is about to be rerendered.

So in the end, the console looks like that:

```
render
unmount
render
unmount
render
unmount
render
unmount
render
unmount
```

You can see that every `render` (when the `useEffect` main callback gets executed) is accompanied by respective `unmount` (when the cleanup function is executed). 

Those two "phases" - effect and cleanup - always go in pairs.

So we see that this model differs from traditional lifecycle callbacks of a class components. It seems to be a bit stricter and more opinionated. 

But why was it designed this way? In order to find out, we need to learn how `useEffect` hook cooperates with component props.

## useEffect & props

Our `Wrapper` component already has a state - `count` - that we can pass into `Example` component, to see how its `useEffect` will behave with the props.

We modify `Wrapper` component in the following way:

```jsx
<Example count={count} />
```
And then we update the `Example` component itself:

```jsx
const Example = ({ count }) => {
    // no changes here

    return <div>{count}</div>;
};
```
It turns out that simply passing the counter as a prop or even displaying it in `div` element of the component does not change the behavior of the hook in any way.

What is more, using this prop in `useEffect` behaves as we would expect, while also giving us a bit more insight into how `useEffect`s main callback and cleanup functions are related.

This code, where we simply add `count` prop to our logs:

```jsx
const Example = ({ count }) => {
    useEffect(() => {
        console.log(`render - ${count}`);
        return () => {
            console.log(`unmount - ${count}`);
        };
    });

    return <div>{count}</div>;
};
```
will result in the following output, when you start clicking on the counter button:

```
render - 0
unmount - 0
render - 1
unmount - 1
render - 2
unmount - 2
render - 3
unmount - 3
render - 4
unmount - 4
```
This might seem like a trivial result, but it enforces what we learned about the main callback of `useEffect` and its cleanup function - they always go in pairs. 

Note that each cleanup function even utilizes the same props as its respective callback.

For example first callback has count set to 0 and its cleanup function utilizes the same value, instead of 1, which belongs to the next pair of the effect and cleanup.

This is a key to the design of the `useEffect` hook. Why is that so important, you might ask?

Imagine for example that your component has to establish a connection to a service with a following API:

```js
class Service {
    subscribe(id) {},
    unsubscribe(id) {},
}
```

This service requires you to unsubscribe with exactly the same `id` that you used to subscribe to it in the first place. If you don't do that, you will leave an opn connection, which will cause leaks that ultimately might even crash the service!

Luckily `useEffect` enforces a proper design with its architecture.

Note that if `id` required by the `Service` is passed via props to the component, all you have to do is to write inside that component:

```js
useEffect(() => {
    service.subscribe(id);
    return () => {
        service.unsubscribe(id);
    };
});
```
As we have seen with our logging examples, `useEffect` will make sure that each `subscribe` is always followed by `unsubscribe`, with exactly the same `id` value passed to it.

This architecture makes writing sound and safe code very straightforward, no matter how often the component updates and no matter how frantically its props are changing.

## Controlling the updates

For people who got used to class component lifecycle methods, `useEffect` often seems limiting at the beginning. 

How do you add an effect only at the very first render?

How do you run a cleanup function only at the end of components life, instead of after every rerender?

In order to find out the answers to those questions, we need to describe one last mechanism that `useEffect` offers to us.

As a second argument, `useEffect` optionally accepts an array of values. Those values will be then compared to the previous values, when deciding if the effect should be ran or not. 

It works a bit like `shouldComponentUpdate` for side effects. If the values changed, the effects will be ran. If none of the values changed, nothing will happen.

So we can edit our `Example` component like so:

```jsx
const Example = ({ count }) => {
    useEffect(() => {
        // everything here stays the same as before
    }, [count]);

    return <div>{count}</div>;
};
```

Because our `useEffect` function used `count` prop and because we want to log a string to the console every time the count changes, we provided a second argument to the `useEffect` - an array with only one value, namely the prop which we want to observe for changes.

If between rerenders the value of `count` does not change, the effect will not be ran and no log with appear in the console.

In order to see that it's really what happens, we can edit our `Wrapper` component:

```jsx
const Wrapper = () => {
    // everything here stays the same as before

    return (
        <div>
            <button onClick={updateCount}>{count}</button>
            {count < 5 && <Example count={count} />}
            {count < 5 && <Example count={-1} />}
        </div>
    );
};
```
You can see that we are now rendering two `Example` components. One - as before - gets passed `count` value as a prop, while the other gets always the same value of -1.

This will allow us to compare the difference in the console outputs, when we click repeatedly on the counter button. Just remember to include `[count]` array as a second parameter to `useEffect`.

After clicking on the counter several times, we get:

```
render - 0
render - -1 // this was logged by the second component
unmount - 0
render - 1
unmount - 1
render - 2
unmount - 2
render - 3
unmount - 3
render - 4
unmount - 4
unmount - -1 // this was logged by the second component
```

So, as you can see, if you include `count` in the array of the second argument to `useEffect`, the hook will only be triggered when the value of the prop changes *and* at the beginning and the end of the life of the component.

So, because our second `Example` component had -1 passed as `count` the entire time, we only saw two logs from it - when it was first mounted and when it was dismounted (after `count < 5` condition began to be false).

Even if we would provide some other props to the `Example` component and those props would be changing often, the second component would still log only twice, because it now only watches for changes in `count` prop. 

If you wanted to react to changes of some other props, you would have to include them in the `useEffect` array.

On the other hand, in the first `Example` component from the snippet, value of the `count` prop was increasing by one on every click on the button, so this component was making logs every time.

Let's now answer a questions that we asked ourselves earlier. How do you make a side effect that runs only at the beginning and at the end of components lifecycle?

It turns out that you can pass even an empty array to the `useEffect` function:

```js
useEffect(() => {
    console.log('render');
    return () => {
        console.log('unmount');
    };
}, []);
```

Because `useEffect` only triggers callbacks at the mount and unmount, as well as value changes in the array, and there is no values in the array, the effects will be called only at the beginning and the end of components life.

So now in the console you will see `render` when the component gets rendered for the first time and `unmount` when it disappears. Rerenders will be completely silent.

## Summary

That was probably a lot of to digest. So let's make a brief summary, that will help you remember the most important concepts from this article:

* `useEffect` hook is a mechanism for making side effects in functional components. Side effects should not be caused directly in components body or `render` function, but should always be wrapped in a callback passed to `useEffect`.
* You can optionally return in the callback another callback, which should be used for cleanup purposes. The main callback and cleanup callback are always triggered in pairs, with exactly the same props.
* By default `useEffect` callback (and corresponding cleanup) is ran on initial render and every rerender as well as on dismount. If you want to change that behaviour, add an array of values as a second argument to the `useEffect`. Then the effects will be ran only on mount and unmount of the component or if the values in that array changed. If you want to trigger the effects *only* on mount and unmount, simply pass an empty array.

So that's it! I hope this article helped you deeply understand how `useEffect` works.

It might seem like a basic and easy hook, but now you see just how much complexity and subtlety is behind it.

If you enjoyed this article, considered following me on [Twitter](https://twitter.com/m_podlasin), where I will be posting more articles on JavaScript programming.

Thanks for reading!

<span>(Cover Photo by <a href="https://unsplash.com/@milandegraeve?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">milan degraeve</a> on <a href="https://unsplash.com/s/photos/hook?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a>)</span>