Rethinking the component model with Hooks
Tags: react
Published: 2019-02-11 05:11:50

---
title: Rethinking the component model with Hooks
published: true
description: Compared to classes, hooks expose edge-cases early. This is great for correctness of the application, but annoying to deal with early on.
tags: reactjs
---

> This post was sent to [my newsletter](https://sid.studio/newsletter) last week.
>
> Thanks to [Dan Abramov](https://twitter.com/dan_abramov) for reviewing it and suggesting improvements.

If you're a fan of React, you might have already heard that the release with Hooks (v16.8) is here.


I've been playing with the alpha version for a few weeks now and I really like it. The adoption hasn't been all rainbows and unicorns though.

Learning `useState` and `useReducer` was pretty straightforward and has improved how I handle state.

I wrote about `useState` [in an earlier post](https://sid.studio/post/react-hooks). Here's the short version:

```js
function Counter() {
  /*
    create a new state pair with useState,
    you can specify the initial value
    as an argument
  */
  const [count, setCount] = useState(0)

  /*
    create a function to increase this count
    you have access to the current count as it
    is a local variable.

    Calling setCount will trigger a re-render
    just like setState would.
  */
  function increase() {
    setCount(count + 1)
  }

  return (
    <div>
      {count}
      <button onClick={increase}>Increase</button>
    </div>
  )
}
```

&nbsp;

However, I really struggled with the `useEffect` hook.

> The Effect Hook lets you perform side effects in function components.

Side effects can mean anything from updating the document title to making an API request. Anything that happens outside your React render tree is a side effect for the component.

With classes, you would typically do this in `componentDidMount`. With hooks, it looks like this:

```js
import React, { useState, useEffect } from 'react'

// username is passed in props
render(<UserProfile username="siddharthkp" />)

function UserProfile(props) {
  // create a new state pair with empty object as default
  const [user, setUser] = useState({})

  // create a pair for loading state
  const [loading, setLoading] = useState(false)

  // Similar to componentDidMount
  useEffect(function() {
    // set loading to true at start
    setLoading(true)

    // fetch the user's details
    // username is passed in props
    fetch('/get-user?username=' + props.username)
      .then(response => response.json())
      .then(user => {
        setUser(user) // set user in state
        setLoading(false) // set loading to false
      })
  })

  if (loading) return <div>Fetching user... </div>
  else return <div>Hi {user.name}</div>
}
```

This feels familiar. It looks like `componentDidMount` in a different suit.

Well, it doesn't have the same way. The above code has a bug!

Look at this preview, it's on an infinite loop of fetching user and re-rending it (and not just because it's a gif!)



![fetch user](https://sid.studio/blog/28/fetch-1.gif?v=2)



`componentDidMount` is called after the component has mounted. It fires just once.

On the other hand, the effect inside `useEffect` is applied on every render by default.

This is a subtle shift in the mental model, we need to change how we think about the component lifecycle - instead of mount and update, we need to think in terms of _renders and effects_

`useEffect` lets us pass an optional argument - an array of `dependencies` that informs React when should the effect be re-applied. If none of the dependencies change, the effect will not be re-applied.

```js
useEffect(function effect() {}, [dependencies])
```

Some folks find this annoying - it feels like something that was simple is now complex with no benefit.

The benefit of `useEffect` is that it replaces three different API methods (`componentDidMount`, `componentDidUpdate` and `componentWillUnmount`) and hence makes you think about all those scenarios from the start - first render, update or re-render and unmount.

In the above component, the component should fetch user details again when we want to show a different user's profile, i.e. when `props.username` changes.

With a class component, you would handle this with `componentDidUpdate` or `getDerivedStateFromProps`. This usually comes as an after thought and until then the component shows stale data.

With `useEffect`, you are forced to think about these use cases early on. We can pass `props.username` as the additional argument to `useEffect`.

```js
useEffect(
  function() {
    setLoading(true) // set loading to true

    // fetch the user's details
    fetch('/get-user?username=' + props.username)
      .then(response => response.json())
      .then(user => {
        setUser(user) // set user in state
        setLoading(false) // set loading to false
      })
  },
  [props.username]
)
```

React will now keep track of `props.username` and re-apply the effect when it changes.

&nbsp;

Let's talk about another kind of side effect: Event listeners.

I was trying to build a utility that shows you which keyboard button is pressed. Adding a listener on `window` to listen to keyboard events is a side effect.

![key debugger](https://sid.studio/blog/28/key-debugger-1.gif?v=2)

### Step 1: Add event listener in effect

```js
function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key) // set key in state
  }

  useEffect(function() {
    // attach event listener
    window.addEventListener('keydown', handleKeyDown)
  })

  return <div>Last key hit was: {key}</div>
}
```

This looks similar to the previous example.

This effect will be applied on every render and we will end up with multiple event listeners that fire on the same event. This can lead to unexpected behavior and eventually a memory leak!

### Step 2: Clean up phase

`useEffect` gives us a way of cleaning up our listeners.

If we return a function from the effect, React will run it before re-applying the effect.

```js
function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  useEffect(function() {
    window.addEventListener('keydown', handleKeyDown)

    return function cleanup() {
      // remove the event listener we had attached
      window.removeEventListener('keydown', handleKeyDown)
    }
  })

  return <div>Last key hit was: {key}</div>
}
```

##### Note: In addition to running before re-applying an effect, the cleanup function is also called when the component unmounts.

Much better. We can make one more optimisation.

### Step 3: Add dependencies for re-applying effect

Remember: If we don't pass dependencies, it will run on every render.

In this case, we only need to apply the effect once, i.e. attach event listener on window once.

Unless the listener itself changes, of course! We should add the listener `handleKeyDown` as the only dependency here.

```js
function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  useEffect(
    function() {
      window.addEventListener('keydown', handleKeyDown)

      return function cleanup() {
        window.removeEventListener('keydown', handleKeyDown)
      }
    },
    [handleKeyDown]
  )

  return <div>Last key hit was: {key}</div>
}
```

The `dependencies` are a powerful hint.

- no dependencies: apply the effect on every render
- `[]`: only apply on first render
- `[props.username]`: apply when the variable changes

&nbsp;

We can even abstract this effect out into a _custom hook_ with cleanup baked in. This makes our component worry about one less thing.

```js
function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  useEventListener('keydown', handleKeyDown)

  return <div>Last key hit was: {key}</div>
}

// re-usable event listener hook with cleanup
function useEventListener(eventName, callback) {
  useEffect(function() {
    window.addEventListener(eventName, callback)

    return function cleanup() {
      window.removeEventListener(eventName, callback)
    }
  }, [])
}
```

##### Note: `useEventListener` as defined above works for our example, but is not the complete implementation. If you're curious what a robust version would look like, [see this repo](https://github.com/siddharthkp/use-event-listener).

&nbsp;

Let's add one more feature to our `KeyDebugger`. After a second, the key should disappear until another key is pressed.

![key debugger](https://sid.studio/blog/28/key-debugger-2.gif?v=2)

That's just a `setTimeout`, should be easy right?

In `handleKeyDown`, we can unset the key after a delay of a second. And as responsible developers, we will also clear the timeout in the cleanup function.

```js
function KeyDebugger(props) {
  const [key, setKey] = useState(null)
  let timeout

  function handleKeyDown(event) {
    setKey(event.key)

    timeout = setTimeout(function() {
      setKey(null) // reset key
    }, 1000)
  }

  useEffect(function() {
    window.addEventListener('keydown', handleKeyDown)

    return function cleanup() {
      window.removeEventListener('keydown', handleKeyDown)
      clearTimeout(timeout) // additional cleanup task
    }
  }, [])

  return <div>Last key hit was: {key}</div>
}
```

This code has become a little more complex than before, thanks to the two side effects happening in the same effect - `setTimeout` nested within a `keydown` listener. This makes the changes harder to keep track of.

Because the two effects are nested, we couldn't reap the benefits of our custom hook as well. One way to simplify this code is to separate them into their own respective hooks.

##### Sidenote: There is a very subtle bug in the above code which is difficult to surface - Because timeout is not cleared when `key` changes, old callbacks will continue to be called which can lead to bugs.

```js
function KeyDebugger(props) {
  const [key, setKey] = useState(null)

  function handleKeyDown(event) {
    setKey(event.key)
  }

  // keyboard event effect
  useEventListener('keydown', handleKeyDown)

  // timeout effect
  useEffect(
    function() {
      let timeout = setTimeout(function() {
        setKey(null)
      }, 1000)

      return function cleanup() {
        clearTimeout(timeout)
      }
    },
    [key]
  )

  return <div>Last key hit was: {key}</div>
}
```

By creating two different effects, we are able to keep the logic separate (easier to track) and define different dependencies for each effect. If we want, we can extract the timeout effect into a custom hook as well - [useTimeout](https://github.com/siddharthkp/use-timeout).

##### Sidenote: Because this component runs cleanup on every `key` change, it does not have the sidenote bug from before.

I know it sounds difficult at first, but I promise it will become easy with a little practice.

Hope that was useful in your journey.

Sid

P.S. I'm working on a React Hooks course - [Learn React Hooks by building a game](https://react.games?s=d). I really believe it is going to be amazing.

[![react.games preview](https://sid.studio/blog/28/react-game.jpg)](https://react.games?s=d)

Visit [react.games](https://react.games?s=d) to watch a preview of the course and drop your email to get a discount when it launches (March 15).



---

[![newsletter](https://sid.studio/newsletter-snapshot.png?v=4)](https://sid.studio/newsletter)