React: Lessons from the Trenches - useEffect x Infinity
Tags: react, webdev, javascript, frontend
Published: 2023-03-23 13:21:46

Hey! My name is Peter and I’m a software team-lead, working on [Codux](https://codux.com/?utm_source=peterArticle1&utm_medium=Top) by Wix.com.

I’ve been building user interfaces with React for more than 6 years and have made tons of mistakes during this time. From memoizing almost everything in my component to drilling props like I’m looking for oil — I fell into every trap. I’ll try to break down a couple of issues I’ve dealt with and hopefully, it’ll help you jump over pitfalls in the future. Some articles in this series will address bad practices and design concerns while others focus on specific bugs in special use cases.

Let’s start with a prevalent one, updating a state within `useEffect` can cause an infinite loop by re-rendering the component to infinity.

![`Maximum update depth exceeded` warning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gb5unv6pd3hf6e5wu986.gif)

What we see above is a warning (“Maximum update depth exceeded”) indicating that there’s an operation occurring too many times, one after the other.

This can happen in several scenarios.

```js
const [superheroes, setSuperheroes] = useState([]);

const updateSuperheroes = () => {
   const newSuperhero = generateSuperhero();
   setSuperheroes([...superheroes, newSuperhero]);
};

useEffect(() => {
  updateSuperheroes();
}, [updateSuperheroes]);
```

Consider the example above.
First of all, we know that every state update triggers a re-render, meaning that the code written inside the component function gets executed. We also know that `useEffect` executes when one of two conditions is met after a render:

1. A component is mounted

2. One of the dependencies (declared in `useEffect`’s dependency array) has changed.

> Keep in mind that `useEffect`’s dependency array should reflect the usage. Omitting a dependency that is used inside the `useEffect` can cause stale data issues, while including a dependency that does not impact the effect can cause unnecessary re-renders.

With these in mind, we can start debugging the code above.
We can see that `updateSuperheroes()` is executed inside a `useEffect`, which requires us to declare it as a dependency.
`updateSuperheroes` adds superheroes to the state by creating a copy of the `superheroes` array and adding new entries to it.

When the `superheroes` state gets updated, the component re-renders with the new state, the `updateSuperheroes` function is created anew which causes `useEffect` to trigger, restarting this cycle.

## What to do?

To prevent something from being created multiple times over multiple renders we often take advantage of memoization. React offers several tools to memoize our functions and values. `useCallback` is used for functions and `useMemo` is used for values such as objects or arrays.

> Primitives that require a minimal computation don't need to be memoized since they are easily comparable, on the other hand, objects, functions, arrays and other complex data structures, could be created anew with every render if declared inside the component.

In our case, `updateSuperheroes` is a function, and should be memoized, therefore, we should leverage `useCallback`.

```js
const [superheroes, setSuperheroes] = useState([]);

const updateSuperheroes = useCallback(() => {
   const newSuperhero = generateSuperhero();
   setSuperheroes([...superheroes, newSuperhero]);
}, [superheroes]);

useEffect(() => {
  updateSuperheroes();
}, [updateSuperheroes]);
```

But this is not enough. We can see that `useCallback` has a dependency on `superheroes` that gets changed on every run of `useEffect`, making this memoization redundant. For such cases, React provides us with another way to update the state without being dependent on the value.

Every state setter can get two types of the first argument.

One, a new state:
```js
const [state, setState] = useState([])
// ...
setState([...state, 'something new']);
```
Or two, a function that gets the previous state as an argument and returns a new state.
```js
const [state, setState] = useState([])
// ...
setState(previousState => [...previousState, 'something new']);
```

Using the latter way in our example will fix the infinite loop since there is no dependency on the state anymore. It will trigger only when the component mounts.

```js
const [superheroes, setSuperheroes] = useState([]);

const updateSuperheroes = useCallback(() => {
   const newSuperhero = generateSuperhero();
   setSuperheroes((previousSuperheroes) => [
      ...previousSuperheroes,
      newSuperhero
   ]);
}, []);

useEffect(() => {
  updateSuperheroes();
}, [updateSuperheroes]);
```

In some cases, you can extract values to an outer scope (outside of the component) when a value is not dependent on any specific component code.

For example, instead of:
```js
const emptyObject = useMemo(() => ({}, []);
```

Move `emptyObject` outside of the component code:

```js
const emptyObject = {}

function Superheroes() {
// ...
}
```

This will prevent the creation of a new object instance for every render without the need to memoize it over time.

## What can be so complex?

Now that we know how to handle the simple case of a state update within a `useEffect`, let’s delve into the more complex scenarios.

In the example above we could very easily tell which dependency in our `useEffect` is causing the infinite loop. Debugging more complex cases where `useEffect` has a large number of dependencies (and each one of them might have its own set of dependencies) is a bit harder. Finding the culprit and maintaining this code in general requires more effort.

I’ve seen colossal dependency arrays across many projects, and here are a couple of recommendations for how to deal with them.

### 1. Separate your gigantic `useEffect` into a few smaller ones

```js
useEffect(() => {
   // ...
}, [
   peopleProvider,
   engineersProvider,
   doctorsProvider,
   generateUniqueId,
   initializeApp
]);
```

Converting this example, where a `useEffect` gets a large number of possibly non-related dependencies, into this:
```js
useEffect(() => {
   // ...
}, [peopleProvider]);

useEffect(() => {
   // ...
}, [engineersProvider]);

useEffect(() => {
   // ...
}, [doctorsProvider]);

useEffect(() => {
   // ...
}, [generateUniqueId, initializeApp]);
```

This way we can scope our side effects into separate, smaller `useEffect` declarations to prevent triggering irrelevant handlers.
In other words, updated dependencies will not trigger a side effect or a state update for a non-relevant functionality.

### 2. Debug the dependencies that are changing on every render

If you still couldn’t find the main cause of an infinite render and you still don’t know which state gets updated and what handler is causing it, I recommend implementing or using a helper hook ([such as this](https://github.com/damiangreen/use-trace-update)) that logs the changed dependencies for each render.

```js
useEffect(() => {
   // ...
}, [
   peopleProvider,
   engineersProvider,
   doctorsProvider,
   generateUniqueId,
   initializeApp
]);

useTraceUpdate({
   peopleProvider,
   engineersProvider,
   doctorsProvider,
   generateUniqueId,
   initializeApp
});
```

This will log only the changed values, indicating which dependency is changing every render leading to an infinite loop.

## Conclusion

Fixing and avoiding `useEffect`’s infinite renders can be achieved by applying the following measures:
- Keep your dependency arrays short and concise. Scope the `useEffect` you’re creating to a specific minimal context and avoid mixing dependencies that have different intents.
- Memoize complex values and functions passed to your hooks and components.
- Avoid depending on a state and setting it in the same `useEffect`, this is a *very* fragile situation that can break easily. If you must have such a case, you can take advantage of the option to set the state using a function provided to `setState` and not be dependent on the previous value.
- Find the always-changing dependency (the one that is causing the infinite render) by debugging your dependencies. Try locating the value that is causing the infinite renders and memoize it correctly.

I hope you've found this article helpful, and I look forward to see y'all at the next one!