By the time I got interested in React, the transition from class-based to functional components was well underway (thank goodness) and the smart kids were using hooks everywhere while others were scratching their heads.

One thing that isn’t immediately obvious in this move toward the function-based approach is, what happened to component lifecycle methods? One might expect native hooks like useDidMount or useWillUnmount.

Alas, no, but a conceptually different animal: useEffect comes to the rescue. useEffect will run after a component renders. Here’s my hover message on the useEffect signature from VS Code:

useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void
Imperative function that can return a cleanup function
Accepts a function that contains imperative, possibly effectful code.
@version — 16.8.0
@see — https://reactjs.org/docs/hooks-reference.html#useeffect

OK, so practically speaking, we see that useEffect takes a callback and that we have a couple important choices:

  1. Whether to use a dependency list (and what to put in it)
  2. Whether to return a cleanup function (and what to return)

Here’s how useEffect does double duty of giving us access to some lifecycle event triggers AND allowing us to take state change dependent actions.

In it’s most basic form (without a dependency list), useEffect will run its associated callback after every render, usually not something you want to do.

// no dependency list

  useEffect(() => {
    console.log('component rendered');
  });

What if we only want a function to run when a component mounts though? We can use an empty dependency list (an empty array) to tell useEffect that it is dependent on something, but that something will only change twice – at instantiation and removal. What this means is that it will only run when:

  • the component mounts (it runs the callback)
  • the component unmounts (it runs the cleanup function, if provided – more on that below).
 // empty dependency list

  useEffect(() => {
    console.log('component mounted');
  }, []);

Nice, now what if we want to trigger an even when there is an update of some specific piece of state (or a passed prop)? We simply have to add that piece of state to the dependency array.

 // populated dependency list

  useEffect(() => {
    console.log(`myState just changed to ${myState}`);
  }, [myState]);

Note that the dependency array can technically contain any number of valid array members [myState, anotherPieceOfState, ...]. And, if you want useEffect to be triggered by an object of some kind (that inherently doesn’t change due to being passed by reference) you can instead put a changing property into the dependency array or JSON.stringify() the object to make changes detectable.

Great, now what about that ‘cleanup’ function…

If your useEffect creates anything that needs cleaning up, like adding event listeners or setInterval, etc. then you can add a return statement to your callback. You return another callback that will get run at unmount.

Here is a practical example that I wrote yesterday for an event listener that attaches and detaches when a modal is opened or closed.

import React, { useState, useEffect } from 'react';
 
  useEffect(() => {
    if(modalState)
      document.body.addEventListener('click', clickEventListener);
    else
      document.body.removeEventListener('click', clickEventListener);
    return () => document.body.removeEventListener('click', clickEventListener);
  }, [modalState]);

There you have it, useEffect is one of those not immediately intuitive, yet very fundamental, tools for React development.

Last modified: August 13, 2020

Author