React's useEffect & useState hooks
Tags: javascript
Published: 2021-05-06 08:05:40

Disclaimer: This is not a basic introduction to hooks. There are many great tutorials out there that cover that part, like the [React docs](https://reactjs.org/docs/hooks-reference.html) themselves.

---

As part of the Junior Engineering Program at 99designs, I did a little deep dive into the `useEffect` and `useState` hooks of React. 

It was quite interesting for me to learn on a topic that I was relatively familiar with (I've used both those hooks heaps of times) and to see how much there still was that I didn't quite properly understand yet.

My starting point was the `why` of things. I looked through the original motivation behind introducing hooks, `useEffect` and `useState` in general. You can find this doc [here](https://reactjs.org/docs/hooks-intro.html#motivation).
<br />

### Why useEffect?

![](https://i.imgur.com/Kajbief.gif)


The main motivation behind introducing the `useEffect` hook was to make complex components easier to manage and read. 

> Hooks let you split one component into smaller functions based on what pieces are related.

Before the `useEffect` hook and `function components` were introduced, the standard way of managing `side effects` inside `class components` were lifecycle methods.

However, they presented you with a particular problem where you had to split your code logic based on **when** something was happening, not **what** was happening. As a result, your code was hard to read and difficult to test as well.

Here you can see a very conceptual example of this problem:

```javascript
 componentDidMount() {
    // do x immediately after component has mounted 
    // also do y immediately after component mounted
 }
 componentDidUpdate() {
    // only do y when component has updated (but not on initial render)
 }
 componentWillUnmount() {
    // cleanup x immediately before component has unmounted
 }
```
You can see that our code is all over the place. `componentDidMount` contains logic related to functionality `x` **AND** `y`, while `componentDidUpdate` just contains logic related to functionality `y`, and `componentWillUnmount` on the other hand again contains logic only related to `x`. This makes code hard to read and test as I mentioned earlier.

So in comes our `useEffect` hook which helps us solve this issue with a much cleaner approach that allows us to split our logic based on the **what** of things, not the **when**.

By default, `useEffect` runs after the first render and after every update as well, so basically after every render, to put it simpler.

Let's return to our conceptual example from before and see how `useEffect` is solving our previously described problem. 

```javascript
useEffect(() => {
   // do x immediately after component has mounted
   // cleanup x immediately before component has unmounted
}, [])
```
```javascript
useEffect(() => {
   // only do y when component has updated (but not on initial render)
}, [])
```

You can see how we are now able to group based on the different things that are happening and `x` and `y` are no longer mingled and mixed up. 

The result: easier to read and much easier to test as well.

At this point, it is also worth noting that React strongly encourages you to use several effects in your component if you have a lot of different things happening. So don't worry if you end up with 3 different `useEffect` inside your component, that's actually considered good practice.

<br />
### The dependency array of useEffect

![](https://i.imgur.com/mNEya74.gif)

So we've seen the first argument that our `useEffect` hook takes, a function where you'll outline all the magical things you want to happen. But the `useEffect` hook also takes in a second argument, often called `dependency array`, which is extremely important, and for me, this deep dive really helped me better understand how this second argument works, why it's so important, and what are some gotchas.

React introduced the dependency array to improve performance. The way it works is relatively straightforward if you're working with primitive values such as `booleans`, `numbers`, or `strings`. There are three scenarios that you can create:

**1. Not passing the dependency array - not really recommended**

If you don't pass a second argument (even if it's empty) your effect will re-run on every re-render, which isn't great for performance
```javascript
useEffect(() => {
    // no dependency array - runs on every re-render
})
```
**2. Passing an empty dependency array**

If you just pass an empty array as a second argument, you're basically telling React that your effect has NO dependencies and it'll never re-run

```javascript
useEffect(() => {
    // empty dependency array - effect has NO dependencies and never re-runs
}, [])
```
**3. Passing values to your dependency array - probably the most used use-case**

The rule of thumb is that if you are using any props or state variables in your effect, you should pass them again to your dependency array.
This way React can keep track of when one of these values has updated and consequently will re-run your effect on re-render.
    
```javascript
useEffect(() => {
    // dependency array with values - if one of the values has changed, 
    // effect will re-run
}, [value1, value2])
```

As I mentioned earlier, this works pretty well when you're dealing with primitive values. With more complex values like objects, arrays, and functions, however, you need to pay a bit more attention to detail and might come across some use cases that need a bit of extra work.

![](https://i.imgur.com/1V1eVyN.gif)

The reason why complex values don't work the same way as primitive values lies in the way React, or rather JavaScript handles those values. Under the hood, React uses the [Object.is method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#description).

So what does that mean exactly?

When you have an object, array, or function in your component (whether that's a state variable or props) React stores a reference to that object in memory (like an address where that object lives in memory).

The problem is that you don't have any guarantees that on the next re-render the reference to your object will be the same, in fact, it's pretty likely that it won't be.

As a consequence, when React compares the value you have passed to the dependency array in your `useEffect`, to the original one, they won't be the same because their "address" in memory has changed on the re-render and thus, even if your value hasn't been updated, your effect will re-run again and again because the two values reference a different object in memory (even though to you they look the same).

Let's look at an example:
    
```javascript
const Team = ({ team }) => {
  const [players, setPlayers] = useState([])

  useEffect(() => {
    if (team.active) {
      getPlayers(team.id).then(setPlayers)
    }
  }, [team])

  return <Players team={team} players={players} />
}
```

So let's say you have an object that you pass to your component as props. Here we have a `Team` component that takes in a `team` object that looks like this:

```javascript
const team = {
    id: 1,
    name: 'Bulldogs',
    active: true
}
```

On every re-render, the reference to your team object will most likely be different. 

So when you pass it to your dependency array and React checks whether this object has changed or not and whether to run the effect again or not, the comparison will return `false` causing your effect to re-run on every re-render. 

So what can you do to avoid this? There are several possible approaches and I'm just listing a few of them.

**1. Only pass what you really need and use in your `useEffect` hook:**

Let's have a look at our Team component again:

```javascript
const Team = ({ team }) => {
  const [players, setPlayers] = useState([])

  useEffect(() => {
    if (team.active) {
      getPlayers(team.id).then(setPlayers)
    }
  }, [team.id, team.active])

  return <Players team={team} players={players} />
}
```

Inside our effect, we're really just using properties from our team object, namely `team.active` and `team.id` which are primitive values again.

As a result, we can just pass those exact values to our dependency array and thus avoid all the references/address comparison complications mentioned above. Now our effect will only re-run if `team.id` or `team.active` have changed.

**2. Recreate the object to use inside of our effect:**

Let's have a look at another example and assume that for some reason we need the whole `team` object in our `useEffect` and also in our component. 

```javascript
const Team = ({ id, name, active }) => {
  const [players, setPlayers] = useState([])

  useEffect(() => {
    const team = { id, name, active }

    if (team.active) {
      getPlayers(team).then(setPlayers)
    }
  }, [id, name, active])

  const team = { id, name, active }

  return <Players team={team} players={players} />
}
```

We can just recreate the object twice, once inside of our `useEffect` hook and once in our component. It's not very expensive to do that, so you don't have to worry about performance issues when using this approach. It's actually not a bad practice to move everything you need into your effect where possible since this way you clearly know what you're using and depending on.


**3. Memoisation - last resort:**

As a very last resort, if you have some very expensive calculations that you want to avoid re-running on every re-render, you can use React's `useMemo` hook.

```javascript
const Team = ({ id, name, active }) => {
  const team = useMemo(() => createTeam({ id, name, active }), [
    id,
    name,
    active,
  ])
  const [players, setPlayers] = useState([])

  useEffect(() => {
    if (team.active) {
      getPlayers(team).then(setPlayers)
    }
  }, [team])

  return <Players team={team} players={players} />
}
```
Be aware though that using this hook itself is quite expensive, so you should think twice before using it. You can learn more about the `useMemo` hook [here](https://reactjs.org/docs/hooks-reference.html#usememo).
    
<br />
### Cleaning your effect up

![giphy (11)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o08gjdtmp84c61obv1bh.gif) 

Especially when you run timers, events, or subscriptions inside your effect, it can be useful to clean those up before the next effect and when the component unmounts to avoid memory leaks.

The way to go about this is to return a function from your effect that will act as a cleanup.

```javascript
const Team = ({ team }) => {
  const [players, setPlayers] = useState([])

  useEffect(() => {
    if (team.active) {
      getPlayers(team.id).then(setPlayers)
    }
    subscribePlayers(players)
    
    return  (() => unsubscribePlayers(players)) // 'cleans up' our subscription
  }, [team.active, team.id])

  return <Players team={team} players={players} />
}
```

<br />
<br />
### Why useState?

![giphy (12)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zmnb8zn96fgolyc323y0.gif)
 

In a very simple way, `useState` lets you add React state to function components (like `setState` for class components).

A little tip when using this hook: split state into multiple state variables based on which values tend to change together (especially helpful when dealing with objects or arrays) and use it for simple state management. 

If things get more complex in the way you manage state, there are other tools for that.

While I didn't find `useState` as complex as `useEffect`, there are some important characteristics to keep in mind when working with it:

**1. Updating a state variable with the `useState` hook always __replaces__ that variable instead of merging it (like setState does).**
This is quite important when you're dealing with objects or arrays, for example.

If you're just updating one item in an array or one property value of an object, you will always have to spread in the original object or array to not overwrite it with just the part that you're updating.

```javascript
const [team, setTeam] = useState(team)

setTeam({
    ...team,
    team.active: false
})
```
**2. It's asynchronous.**
Quite important to keep in mind that when you call your function that sets state (`setTeam`, for example) it behaves asynchronously, so it just adds your value update request to a queue and you might not see the result immediately.

That's where the `useEffect` hook comes in very handy and lets you access your updated state variable immediately.

**3. You can update state with a callback.**
The `useState` hook gives you access to a so-called functional update form that allows you to access your previous state and use it to update your new state.

This is handy when your new state is calculated using the previous state, so for example:

```javascript
const [count, setCount] = useState(0)

setState(prevState => prevState + 1)
```

**4. Only call `useState` at the top level.**
You cannot call it in loops, conditions, nested functions, etc. When you have multiple `useState` calls, the order in which they are invoked needs to be the same between renderings.

There's so much more to hooks than what I've written down here, but those were the things that I think will help me most moving forward.

I've really enjoyed diving deeper into this topic and realised again just how powerful hooks are. I also feel way more confident using them now and hope that after reading this article you do too.