{ The Rithm Blog. }

React Hooks! - useEffect January 14, 2020

Welcome back to the Rithm School series on React hooks. In this blog post, I'll introduce useEffect, a very useful and potentially confusing new hook in React. useEffect can be used in place of three lifecycle methods: componentDidMount, componentDidUpdate, and componentWillUnmount. The details of how useEffect replaces all three methods is what we will be exploring in this blog post.

A working knowledge of some basics of hooks (especially some understanding of useState) is important for this post. If you haven't read my earlier post introducing hooks, or if you want a refresher, check it out here.

useEffect - What's it for?

So what do we need useEffect for? An easy mental model to start with is to think of useEffect as a replacement for the following three lifecycle methods in React:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

However, the useEffect hook doesn't work quit the same way as those three methods. In fact, by default, useEffect is a function that runs after every render for the life of the component. So how would we be able to replace behavior like componentDidMount? First, let's consider a simple example where useEffect can be used to replace componentDidMount.

useEffect - Replacing componentDidMount

Frequently in class based components, I would use componentDidMount to make an AJAX request to get some data. But how would that look with useEffect? Let's look at the class component, then refactor it to use hooks:

import React, {Component} from 'react';

class StarWars extends Component {
  constructor(props) {
    super(props);
    this.state = {
      characters: []
    }
  }

  async componentDidMount() {
    const resp = await fetch('https://swapi.co/api/people/');
    const data = await resp.json();
    this.setState({characters: data.results})
  }

  render() {

    const characters = this.state.characters.map(
      (c, id) => <li key={id}>{c.name}</li>
    );

    return (
      <ul>
        {characters}
      </ul>
    );
  }
}

export default StarWars;

Notice how this component always makes a request to get people from the Star Wars api and that request never changes for the life of the component. Also notice just how much code we have to write. We need a constructor and two functions in order to load some Star Wars characters. Let's see what this looks like with useEffect instead.

In order to use useEffect, we need to transition our class based component to a function component. And since we are using a function component, we also must use the useState hook. (Just a reminder, if you're not comfortable with useState, read this blog post first). Here is the code:

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

function StarWars() {

  const [characters, setCharacters] = useState([]);

  useEffect(() => {
    async function getCharacters() {
      const resp = await fetch('https://swapi.co/api/people/');
      const data = await resp.json();
      setCharacters(data.results)
    }

    getCharacters();
  }, []);

  const names = characters.map(
    (c, id) => <li key={id}>{c.name}</li>
  );

  return (
    <ul>
      {names}
    </ul>
  );
}

export default StarWars;

We are using useState to keep an stateful array of Star Wars characters. At the bottom of the function component, we map over those characters to get each character's name for display in the list. The important code happens in useEffect. In the useEffect hook, we are making an AJAX request to get the list of people from the Star Wars api. That data is stored in state using the setCharacters method that we got from useState.

Earlier I said that useEffect is run after every render. Our useEffect hook makes a call to setCharacters, which should trigger a rerender and cause an infinte rerendering loop. So what's going on here? Why isn't this code an infinite render loop?

The answer is the second paramter to the useEffect hook. The second parameter is an array of varaibles that the function in useEffect depends on. If all of the dependencies has not changed in the dependency array, then the function passed to useEffect will not be rerun again after a render. In other words, a useEffect hook is only rerun if the dependencies in the dependency array have changed. In our example, we have no dependencies in the dependency array, which means the useEffect hook only runs 1 time, after the first render. If no depedency array parameter is provided (no second paramter to useEffect), then the function in useEffect is run after every render.

Let's change our example to help show when you would want to add a dependency to the dependency array paramter.

useEffect - Dependency Array

To illustrate when you would use a dependency in the array, let's change the Star Wars example a little. Instead of displaying all the people, let's display a single character. First the class based component:

import React, {Component} from 'react';

class StarWarsCharacter extends Component {
  // Assume that props.characterId has the id of the
  // star wars character that we will display
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      birthYear: ""
    }
  }

  async componentDidMount() {
    const resp = await fetch(`https://swapi.co/api/people/${this.props.characterId}`);
    const data = await resp.json();
    this.setState({name: data.name, birthYear: data.birth_year});
  }

  async componetDidUpdate() {
    const resp = await fetch(`https://swapi.co/api/people/${this.props.characterId}`);
    const data = await resp.json();
    this.setState({name: data.name, birthYear: data.birth_year}); 
  }

  render() {

    const {name, birthYear} = this.state;
    return (
      <div>
        <p>Name: {name}</p>
        <p>Birth Year: {birthYear}</p>
      </div>
    );
  }
}

export default StarWarsCharacter;

Notice in this new example, we have a componentDidMount lifecycle method as well as a componentDidUpdate lifecycle method. Both are needed because if the characterId prop ever changes for whatever reason, we would want our component to fetch and display new character information. The issue here is that both componentDidMount and componentDidUpdate do the same thing. We could refactor this code to use a helper function so that there isn't as much duplication, but let's see what it looks like with the useEffect hook instead:

function StarWarsCharacter({characterId}) {
  const [{name, birthYear}, setCharacter] = useState({name: "", birthYear: ""});

  useEffect(() => {
    async function getCharacter() {
      const resp = await fetch(`https://swapi.co/api/people/${characterId}`);
      const data = await resp.json();
      setCharacter({name: data.name, birthYear: data.birth_year}); 
    }

    getCharacter();
  }, [characterId]);

  return (
    <div>
      <p>Name: {name}</p>
      <p>Birth Year: {birthYear}</p>
    </div>
  ); 
}

This example should look similar to the first example with useEffect. The main difference now is that our function being passed to useEffect has an external dependency. Whenever the characterId variable changes, we want to run the function again. Adding characterId to the dependency array (the second parameter to useEffect), tells react that we only want the effect to be run when characterId changes.

What do you think will happen if we remove the dependency array parameter entirely? What will the behavior be? Remove the dependency array, then add the following console.log just before the invocation of getCharacter: console.log("in useEffect"); Give it a try in the browser. What do you see in the console? What happens when you add the dependency array back?

One other important note: the function being passed to useEffect is not async. Rather, there is an async function that is defined in useEffect, and then that function is invoked. That is because useEffect cannot accept async functions.

Let's look at one last example relating to componentWillUnmount.

useEffect - componentWillUnmount

Let's say we are debugging our Star Wars component and we simply want to know when will our component unmount. For the class based example, the solution is simple. We just add the componentWillUnmount lifecycle method:

componentWillUnmount() {
  console.log("Unmounting character component");
}

But how does this work with useEffect? The answer in our case is not so simple. If you read through the react docs on hooks, you'll notice that we can return a cleanup function in useEffect. You might be tempted to implement this:

function StarWarsCharacter({characterId}) {
  const [{name, birthYear}, setCharacter] = useState({name: "", birthYear: ""});

  useEffect(() => {
    async function getCharacter() {
      const resp = await fetch(`https://swapi.co/api/people/${characterId}`);
      const data = await resp.json();
      setCharacter({name: data.name, birthYear: data.birth_year}); 
    }

    getCharacter();

    return () => console.log("Unmounting character component");
  }, [characterId]);

  return (
    <div>
      <p>Name: {name}</p>
      <p>Birth Year: {birthYear}</p>
    </div>
  ); 
}

However, the funciton that we return from useEffect is a clean up function. It runs after every invocation of useEffect. In many cases, this behavior is desired. If we wanted to run some sort of update effect, it is also very likely that we want the clean up for that update to run every time as well. The problem in the example above is that our clean up function and our effect are not related at all. The clean up function will be run any time the characterId prop has been updated, which means that we will see the console.log saying that our component is unmounting even when it is simplying updating. To fix this issue, we can use another useEffect hook:

function StarWarsCharacter({characterId}) {
  const [{name, birthYear}, setCharacter] = useState({name: "", birthYear: ""});

  useEffect(() => {
    async function getCharacter() {
      const resp = await fetch(`https://swapi.co/api/people/${characterId}`);
      const data = await resp.json();
      setCharacter({name: data.name, birthYear: data.birth_year}); 
    }
    getCharacter();
  }, [characterId]);

  useEffect(() => {
    return function logWillUnmount() {
      console.log("Unmounting character component");
    };
  }, []);

  return (
    <div>
      <p>Name: {name}</p>
      <p>Birth Year: {birthYear}</p>
    </div>
  ); 
}

In our separate useEffect, notice that our dependency array is empty again. There are no dependencies on this console.log, so the effect will only be run once (on initial mount). Then our cleanup function (with the console.log) will only be run once: just before the component is unmounted. This way you can effectively debug your component's lifecycle just like with a class based component.

useEffect - Wrap Up

The new useEffect hook can be tricky to master at first. I hope this blog post helps to clear some things up as you transition your mental model to hooks. There is a lot more to understand with useEffect. If you want more examples, the react hooks docs are a great place to start. I would also suggest taking some simple react apps that you have implemented and try to refactor them using hooks.

Good luck! And stay tuned from more hooks tutorials.

Written by Tim

Back to all posts

Get Started with Rithm School