DEV Community

Brett Bloxom
Brett Bloxom

Posted on • Edited on

React Hooks - useEffect

The useEffect hook lets you perform side effects in function components

What are side effects?

Side effects are not specific to React. A side effect is anything that affects something outside the scope of the function/component being executed. Anything that is not the return value is technically a side effect.

A few common examples of side effects

  • Data fetching/Network requests
  • Setting up a subscription to an external data source
  • Manually changing the DOM
  • Accessing the Window object

Basic Syntax

The useEffect hook accepts two arguments: the side effect callback function, and an optional dependency array of state values to watch for changes.

useEffect(sideEffectFunction, [stateToTrack]);
Enter fullscreen mode Exit fullscreen mode

By using this hook, you tell React that your component needs to do something after render. React will remember the effect function you provided, and run it after flushing changes to the DOM and letting the browser paint the screen.

By default, useEffect runs after the first render and after every update. Effects happen after render. React guarantees that the DOM has been updated before it runs the effects.

import React, { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0)
  const min = 0

  // Register the effect you want to run
  useEffect(() => {
    // Update the document title using the browser API 
    document.title = `The current count is ${count}`
  })

  const increment = () => setCount(count + 1)
  const decrement = () => (count > min ? setCount(count - 1) : count)
  const reset = () => setCount(0)

  return (
    <div className='counter'>
      <p className='count'>{count}</p>
      <div className='controls'>
        <button type='button' onClick={increment}>
          Increment
        </button>
        <button type='button' onClick={decrement}>
          Decrement
        </button>
        <button type='button' onClick={reset}>
          Reset
        </button>
      </div>
    </div>
  ) 
}

export default Counter
Enter fullscreen mode Exit fullscreen mode

In the above counter example, we declare the count state variable and set its initial value to 0. We then tell React we need to use an effect for updating the document title.

We pass a function to useEffect, which is our effect that we want to be run after the component renders. Inside of our effect, we set the document title using the document.title browser API. Remember, accessing the browser API and manipulating the DOM directly is a side effect.

The above effect is run on every render, including the first one.

Skipping Effects

The effect hook runs when the component mounts but also when the component updates. In the counter example above, the effect is run on every single render which is ok in this case because count is our only piece of state and we want our effect to be run whenever count changes. This is almost never what you want.

Let's look at an example where not passing an array of dependencies and allowing the effect to be run on every render would cause us some serious trouble.

const Repos = () => {
  const  [userName,  setUserName]  =  useState('')
  const  [repos,  setRepos]  =  useState([])

  useEffect(() => {
    async function  fetchRepos() {
      const  response  = await fetch(`https://api.github.com/users/${userName}/repos`)
      const  repos  = await response.json()

      // our setRepos call tells React to re-render the component.
      // which then calls our useEffect hook again, so on and so forth
      setRepos()
    } 
    fetchRepos().catch(error  =>  console.error(error))
    // this is because we are not passing an array of 
    // dependencies as the second argument to useEffect
  })

  const handleSubmit = (e) => {
    e.preventDefault()
    setUserName(e.target.username.value)
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <label htmlFor='username' placeholder='E.g. gaearon'>
          Enter a Github Username
          <input type='text' id='username' />
        </label> 
        <button type="submit">Fetch Repos</button>
      </form>
      <section  aria-labelledby='repos-label'>
        <h2  id='repos-label'>Github Repositories for {userName}</h2>
        {!repos.length  ? (
          <p>
            <b>Not seeing any repos? Either there are no repos for the user you have provided, they do not exist, or there was an error while fetching. Please try again with a different username.</b>
          </p>
        ) : (
          <ul>
            {repos.map(repo  => (
            <li  key={repo.id}>
              <a  href={repo.html_url}>{repo.name}</a>
            </li>
            ))}
          </ul>
        )}
      </section>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The above example is making a network request for an array of Github repositories for a given username, then spits out a list of links pointing to those repos. When the effect is run, it sets our repos state variable, which tells React to re-render our component, which then triggers our effect which tells React to re-render, so on and so forth sending us into a death loop of renders and network requests until either our browser stops responding or we hit our rate limit of 5000 requests to the GitHub API per hour.

So, we don't want to let our effect run after every single render. One option to prevent this death loop is to pass an empty array of dependencies as the second argument to useEffect. This would tell React to only run our effect on the very first render.

...
  useEffect(() => {
    async function  fetchRepos() {
      const  response  = await fetch(`https://api.github.com/users/${userName}/repos`)
      const  repos  = await response.json()

      setRepos()
    } 
    fetchRepos().catch(error  =>  console.error(error))

  // Passing an empty array of dependencies tells React 
  // to only run our effect on the very first render
  }, [])
  ...
Enter fullscreen mode Exit fullscreen mode

As you probably guessed, this is also NOT what we want as we would like to fetch a new list of repos when we submit our form. With an empty array, submitting the form which updates our userName in state, would not make a new request for the updated user's list of repositories as our effect is only run once, on the very first render.

So, we don't want our effect to be run when repos value is updated and we also don't want it to only run on the very first render. Our solution is to add userName as the only dependency to our effect.

...
  useEffect(() => {
    async function  fetchRepos() {
      const  response  = await fetch(`https://api.github.com/users/${userName}/repos`)
      const  repos  = await response.json()

      setRepos()
    } 
    fetchRepos().catch(error  =>  console.error(error))

  // Now our effect will only run if the value of userName in state is updated
  }, [userName])
  ...
Enter fullscreen mode Exit fullscreen mode

Here is the full solution to our Repos component.

const Repos = () => {
  const  [userName,  setUserName]  =  useState('')
  const  [repos,  setRepos]  =  useState([])

  useEffect(() => {
    async function  fetchRepos() {
      const  response  = await fetch(`https://api.github.com/users/${userName}/repos`)
      const  repos  = await response.json()

      setRepos()
    } 
    fetchRepos().catch(error  =>  console.error(error))
  }, [userName])

  const handleSubmit = (e) => {
    e.preventDefault()
    setUserName(e.target.username.value)
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <label htmlFor='username' placeholder='E.g. gaearon'>
          Enter a Github Username
          <input type='text' id='username' />
        </label> 
        <button type="submit">Fetch Repos</button>
      </form>
      <section  aria-labelledby='repos-label'>
        <h2  id='repos-label'>Github Repositories for {userName}</h2>
        {!repos.length  ? (
          <p>
            <b>Not seeing any repos? Either there are no repos for the user you have provided, they do not exist, or there was an error while fetching. Please try again with a different username.</b>
          </p>
        ) : (
          <ul>
            {repos.map(repo  => (
            <li  key={repo.id}>
              <a  href={repo.html_url}>{repo.name}</a>
            </li>
            ))}
          </ul>
        )}
      </section>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

useEffect & Cleanup

Sometimes, we want to run some additional code after React has updated the DOM. Network requests, DOM mutations, and logging are common examples of effects that donโ€™t require cleanup. We say that because we can run them and immediately forget about them.

Anything that we set up that is recurring such as an interval, subscription, websocket connection, etc. needs to be cleaned up when the component unmounts.

Let's add a twist to our counter component...

const Counter = () => {
  const [count, setCount] = useState(0)

  // Log the count to the console after 3 seconds
  // This effect is not cleaning up after itself    
  useEffect(() => {
    setInterval(() => {
      console.log(`Count: ${count}`)
    }, 3000)
    // not returning a cleanup function here
  }, [count])
...
Enter fullscreen mode Exit fullscreen mode

The problem with not cleaning up our setInterval() is that each time the component re-renders, we register another interval. If we were to update the count from 0 to 1, after three seconds, 0 would be logged to the console then 1 would be logged to the console, then 0, then 1 and so on..

This is because there are now two intervals from two separate renders logging the value of count to the console. Each interval has access to the value of count from its respective render. When the component first rendered, the value was 0, so an interval was started to log 0 every three seconds. When we updated count to 1, React triggered another render, then our effect was called, registering another interval to log the new value of count to the console every 3 seconds.

The first interval was never cleared, so now we have two intervals running at the same time.

To avoid this, we need to return a cleanup function from useEffect for our interval.

const Counter = () => {
  const [time, setTime] = useState(new Date())

  // Log the count to the console after 3 seconds
  useEffect(() => {
    const id = setInterval(() => {
      console.log(`Count: ${count}`)
    }, 3000)
    // Return a function to clear our interval when the component unmounts
    return () => clearInterval(id)
  }, [count])
...
Enter fullscreen mode Exit fullscreen mode

setInterval returns a number which is the id of that interval. We set that number to a variable which we pass to the clearInterval function returned from our effect. Now, when count is updated and our component is unmounted before re-mounting to the DOM, we clean up the previous interval. With this in place, only the current value of count will be logged to the console 3 seconds after updating its value.

Recap

  • The useEffect hook lets you perform side effects in function components;
  • A side effect is anything that affects something outside the scope of the function/component being executed;
  • The useEffect hook accepts two arguments: the side effect callback function, and an optional dependency array of state values to watch for changes;
  • By not passing a dependency array to our effect, it will be run on every single render;
  • If we pass an empty array, the effect will only be run once, on the very first render;
  • To avoid an infinite loop of renders and effect calls make sure you are only passing the state values that your effect depends on in the dependency array;
  • Intervals, subscriptions or anything that is meant to be recurring, should be cleaned up by returning a cleanup function from your effect;

Thanks for reading!

Top comments (1)

Collapse
 
folarmi profile image
folarmi

I found this really helpful, thanks.