Overusing useEffect

Jose Cortés
JobTeaser Engineering
4 min readOct 20, 2022

In 2019 with the arrival of hooks we discovered the hook useEffect. As you probably know, this hook allows you to manage side effects in your React components.

Personally, I have seen myself adding this comment many times in PRs:

In this post I will explain the problems of using useEffect, why people use it so much, some examples and recommendations.

What’s wrong with useEffect?

The useEffect hook exists because sometimes it is absolutely necessary. But, because it is extremely powerful hook, it must be used with care. Its excessive use can have a very negative impact. Let’s see why.

Readability

A component in which there are many useEffects is very difficult to understand. You have probably come across components that use a lot of effects, like this example:

I have purposely put an example ignoring a react-hooks/exhaustive-deps rule because it's especially confusing and difficult to understand. If you don’t already use this react-hooks linter, I strongly recommend you to use it.

It doesn’t matter that you are the author of the code and that you have written it just a week ago, you will probably be like:

The useEffect hook adds reading difficulty to your code as it is much more difficult to know when a rerender is going to trigger and how it is going to affect your other effects. So much so that you may be creating an infinite loop without being aware of it.

The dependencies array

As you may know, the dependency array is used to prevent our effect from being executed in each render. But be careful, the React documentation has always mentioned that this array of dependencies is only an optimization, as we can see in the official documentation in section Tip: Optimizing Performance by Skipping Effects

This means that the code to be executed inside our effects must be able to be executed in each render without breaking the logic of our component. Or to say it another way: we should not trust the dependency array the responsibility of under what circumstances the code inside should be executed.

With the arrival of React 18 you may have seen the problems of having thought of the dependency array as a condition for the trigger of your effect and not as an optimization.

From version 18 onwards in development environment with strict mode enabled, all effects are mounted, unmounted and remounted again. Why? Because they are preparing the ground for a future where components can be unmounted while saving the state. And they can do it because, as I said before, the dependency array has always been just an optimization, an effect should be able to be executed as many times as needed without breaking the logic.

And although you will find it in hundreds of StackOverflow issues, the solution is not to disable strict mode 😅. Strict mode is highly recommended to avoid problems in the future.

Why is it used so much?

My theory is that when we started using functional components with hooks, we immediately made equivalences between class component lifecycle methods and useEffect:

componentDidMount is a useEffect with an empty dependency array….

or

componentDidUpdate is a useEffect with an array of dependencies […props].

Not exactly the same, since they are not executed at exactly the same time, but it's true for most cases.

The biggest problem with this approach is that you don’t change your mental model and you keep designing components thinking in translated lifecycle methods instead of thinking in alternatives where our components have to manage less side effects. It is like trying to speak another language using the way we construct sentences in our own language: it may work, but it is not optimal.

Common examples of wrong use

  1. For me the most common example is the one in which we create a state to change it in an event handler and then do something in an effect:

instead of just:

Even if you need to create a controlled component with an internal state, it is better to do without the useEffect and execute the code directly in the callback:

2. Another example a little more complex in which we use a custom hook to request data and we want to send a tracking event:

instead of just:

In a nutshell

  • Think about useEffect as a last resource to synchronize something external to the component.
  • In priority derive the information through props or use callbacks if you can.
  • If you use useEffect mostly for call APIs, you can use React Query or SWR instead to simplify you code.
  • Keep in mind that the dependencies array is just an optimization, your code should keep working even if it is executed more times.

This post is the result of the work we have done in JobTeaser in different sections of the application, such as our Advices site that helps the students to find their way.

As the application grew, we realized that overusing useEffect made the code more difficult to understand. Reducing the number of effects has greatly improved the readability of our applications, and also prepared us for a future version of React 🚀 in which the effects can be executed more times than we thought.

Of course, this is my personal opinion, but I'd love to know what do you think about it.

Have you also had readability problems due to excessive useEffects?

What do you think about the behavior of the StrictMode from React version 18 onwards?

Don’t hesitate to comment. At JobTeaser we love to share because it's the way to grow as a professional.

By the way, if you are interested in knowing more about us, you can check our engineering website. Don’t hesitate to apply if you like what we do! 💚

--

--