Understanding React’s useEffect Hook

React Hooks are awesome and definitely make our lives as React developers much easier. In case you don’t know what they are, Hooks were introduced in React 16.8.0, and they let you use state and other React features without writing a class 💃

The most important thing is that Hooks don’t replace your knowledge of React concepts. Instead, Hooks provide a more direct API to the React concepts you already know: props, state, context, refs, and lifecycle.

Note: if you’re new to React, learn Hooks and functional components first. Only after that, learn the old way of doing things (class components)

This post is not about Hooks in general, but if you want to know more about them and why they exist, the best thing you can do is watching the official React Hooks presentation.

If you haven’t learned Hooks yet, you should do it as soon as possible. And if you’ve been reading about them, you may be a bit confused about the useEffect Hook 😕

The useEffect Hook

Plain and simple, useEffect lets you perform side effects in function components.

Ok… But what are side effects? 😒

Side effects are all the operations that affect your component and can’t be done during rendering. Things like fetching data, subscriptions or manually changing the DOM are all examples of side effects, and things you’ve most likely done in the past.

What are the benefits? ✅

  • Avoiding duplication of code
  • Bringing related code closer together
  • Avoiding bugs
  • Separating separate effects by their purpose, something impossible to do with classes

All these benefits will become clearer once you read the following explanation and examples.

The old way of doing things 🔙

In the past, when we only had class components, we handled side effects using the lifecycle methods.

For example, among other stuff, we used componentDidMount to fetch data from APIs, componentDidUpdate to send data when something changed, and componentWillUnmount to unsubscribe from events.

class MyComponent extends React.Component {
  constructor() {}

  componentDidMount() {
    // Fetch data from API
  }

  componentDidUpdate(prevProps, prevState) {
    // Send data when props change
  }

  componentWillUnmount() {
    // Unsubscribe from events before the component is removed
  }

  render() {}
}

useEffect combines all those three methods into one convenient API.

A paradigm shift ⚡️

To really understanduseEffect, we need to change the way we think about components and state change.

When we used classes, we had to think about when. The question we needed to answer was when does this effect run?

useEffect is different.

It exists to synchronize the state of the world with the state of the component.

So the question now is: which state does this effect synchronize with?

You need to stop thinking about lifecycles and time, and start thinking about state and synchronization with the DOM.

How does useEffect work? ⚙️

By default, useEffect runs after every render, including the first one. React guarantees the DOM has been updated by the time it runs the effects.

When you use useEffect, you’re telling React that your component needs to do something after rendering. You pass a function (the “effect”), and React will remember it and call it later, after performing the DOM updates.

function MyComponent() {
  React.useEffect(() => {
    // this side effect will run after every render
  });

  return ...
}

What about cleanups? 🗑️

We said earlier that useEffectalso “replaces” componentWillUnmount method.

A common use case for this method is to unsubscribe from events before the component is finally unmounted.

To replicate that behavior with useEffect, you just need to return a function. React will take care of everything and run it when it is time to clean up.

function MyComponent() {
  React.useEffect(() => {
    // this side effect will run after every render
    return function cleanup() {
      // this code will run before the component is unmounted
    }
  });

  return ...
}

The dependencies array

Of course, running all effects on every render might lead to performance issues (and even infinite loops in some cases).

However, React doesn’t know what your function does before calling it. It may seem obvious to you that it’s not necessary to run some effect function again, but not for React.

So to fix that, you need to tell React. You can provide a second argument to useEffect, which is a dependencies array.

Think of it like telling React “This function only needs to run if X is updated”. If each of these values is the same between the current and the previous time this effect ran, there’s nothing to synchronize and React will skip the effect.

function MyComponent() {
  React.useEffect(() => {
    // this side effect will run only when value1 changes
  }, [value1]);

  return ...
}

If you pass an empty array as second argument, the function will run just once, after the first render. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.

function MyComponent() {
  React.useEffect(() => {
    // this side effect will run just once, after the first render
  }, []);

  return ...
}

So a summary would be:

  • No dependencies array: the function runs after every render
  • Empty dependencies array: the function runs only after the first render
  • Dependencies array with some values: the function runs only if any of those values change
function MyComponent() {
  React.useEffect(() => {
    // this side effect will run just once, after the first render
  }, []);

  React.useEffect(() => {
    // this side effect will run only when value1 changes
  }, [value1]);

  React.useEffect(() => {
    // this side effect will run after every render
  });

  return ...
}

If you use an empty dependencies array, don’t do it because you think “I need this to run just once, after the first render”; do it because you know that the stuff it’s doing will never get stale.

Be careful with your dependencies array ⚠️

If you use the dependencies array, make sure it includes all values from the component scope (such as props and state) that change over time and that are used by the effect. It’s very common to forget a value or to think that you don’t need it in the array. If you do that, you will produce bugs, because your code will reference stale values from previous renders.

To help with this, always use eslint-plugin-react-hooks.

You can use multiple effects! 😏

You can call useEffect as many times as you want. In fact, it’s recommended to call it several times, in order to separate concerns.

For example, there are some things that you need to do only after the first render and never again, and other things that need to happen after every render.

function MyComponent() {
  React.useEffect(() => {
    // I need to do this just once, after the first render
  }, []);

  React.useEffect(() => {
    // But this other thing needs to be done after every render
  });

  return ...
}

Goodbye code duplication! 👋🏼

useEffect helps to avoid code duplication in a very common case.

Let’s say you need to run an effect after every render. It doesn’t matter if the component has just been mounted or it has just been updated, you need to perform certain side effect in both cases.

To do this with classes you need to duplicate code:

class Mycomponent extends React.Component {
  constructor(props) {}

  componentDidMount() {
    // do something
  }

  componentDidUpdate() {
    // do the same thing again
  }

  render() {
    return ...
  }
}

But with useEffect, the code will run after every render, including the first one.

function MyComponent() {
  React.useEffect(() => {
    // do something
  });

  return ...
}

Keep related code together 👫

Let’s say we have a component where we need to subscribe to an event, and then unsubscribe from it before the component is removed. Here’s how we would do that with classes:

class Mycomponent extends React.Component {
  constructor(props) {}

  componentDidMount() {
    // susbscribe to event X
  }

  componentWillUnmount() {
    // unsubscribe from event X
  }

  render() {
    return ...
  }
}

Note how lifecycle methods force us to split this logic, even though the code in both of them is related to the same effect.

With useEffect, we can keep this code together in the same function:

function MyComponent() {
  useEffect(() => {
    // subscribe to event X
    return function cleanup() {
      // unsubscribe from event X
    };
  });

  return ...
}

useEffect(fn, []) vs. componentDidMount 🥊

For what I’ve told you so far, you may think that’s right. I’ve told you that if the dependencies array is empty, your effect will run only after the first render, which is how componentDidMount works, right?

Well, there is a difference.

useEffect runs after the paint has been committed to the screen. componentDidMount runs before the paint has been commited to the screen.

For most cases, you won’t need to worry about this, because most of your effects will be asynchronous.

But there are some things that require that you read the DOM and synchronously re-render. For example, if you need to measure the layout.

For these cases, there is a separate useLayoutEffect Hook with an API identical to useEffect.

That’s all!

useEffect can be a little tricky, and it requires some practice. I hope this guide helps, and always remember: don’t think about lifecycles, think about synchronizing side effects to state.

If you need more information, you can check React’s official documentation or this awesome guide by Dan Abramov.

Thanks for reading ❤️