React Native

How to manage the useEffect dependency array like a pro?

Useeffect dependency array: TL;DR

You should use ++code>react-hooks/exhaustive-deps++/code> eslint rule and refactor your code in this order:

  • Think about if the useEffect is useful or if it should be refactored
  • Declare the function in the useEffect when it is used only in the useEffect
  • Memoize the function and add it to the dependency array if it is used in several places

For some patterns, but it is dangerous:

  • Ignore the rule if you really know what you're doing

The useEffect hook allows you to perform side effects in a functional component. There is a dependency array to control when the effect should run. It runs when the component is mounted and when it is re-rendered while a dependency of the useEffect has changed. This is powerful, but it is easy to omit dependencies and create bugs in your app.

That's why the React team recommends that you include all the variables you use in the useEffect in the dependency array.

react doc screenshot
Source: React doc

 

To help you to do it, they created the react-hooks/exhaustive-deps eslint rule that detects missing dependencies and gives advice on how to fix them.

However, I have often seen people questioning the usefulness of this rule on my projects or GitHub issues. They say there is no need to put dependencies that never change or it requires memoizing a lot of functions. That's why I'm going to explain why we should use the rule and how we can fix these eslint warnings through an example!

 

a concrete example of useeffect with missing dependencies

Let's take a simple app with a small stack navigator with two screens :

The first Screen is a list of buttons for browsing comments (like a forum):

And the second screen is a list of comments based on the selected category:

Full example

 

There is a re-render button that allows us to see how the component behaves when re-rendered. When the screen is mounted, we fetch comments that have the same ++code>postId++/code> as the one retrieved in the route parameters. Next, we display a list of the fetched comments.

Here is the result :

List of comments

When we go back and select another topic, ++code>postId++/code> change, and we fetch other comments. In this example, our API does not allow us to retrieve the results in a paginated way. So we fetch all the results at once.

But ++code>react-hooks/exhaustive-deps++/code> rule raises a warning :

React Hook useEffect has a missing dependency: 'fetchAndStoreData'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)ESLint

Our app works, so... let's ignore it right?

 

Ok, now we are 3 months later. We want to code a new feature highly requested by users: buttons to navigate between topics inside a topic!

To do this, I added two buttons in the ++code>Comments++/code> screen:++code>
++/code>

Full example

 

Here is the result:

List of comments with navigation buttons

If I click on a button, the button becomes disabled because I am on the last topic. But... the comments are the same as the previous one. If we look at the console, the app is re-rendered, but we don't fetch any data:

Logs of the console

So, after wasting time looking for where the problem comes from, let's look at the rule!

Let's add the missing dependency to our useEffect:

Now it works! But re-render the app, create a new ++code>fetchAndStoreData++/code> function that triggers the useEffect, that sets a new state, that creates a new re-render, etc.

We are in an infinite loop! This causes a lot of fetches, which is a problem for our backend, and a lot of re-renders, which is a problem for the user experience and performance.

Logs of the console

So, we change the dependency by ++code>postId++/code> because we don't want to memoize ++code>fetchAndStoreData,++/code> and it fixes all our problems:++code>++/code>

Until the next problem! Indeed, we still have the eslint warning and this is for a reason!

 

We are 6 months later, and we want to add a new feature: a search feature!

I added a new state, a ++code>TextInput++/code> and some logic in ++code>fetchAndStoreData++/code>:++code>++/code>

Full example

 

Do you see the problem? Yes, we forgot to change the dependencies of our useEffect again. Nothing prevents us from doing it because eslint warning is there since the beginning (or not there if you don't add the rule). We can add ++code>searchText++/code> to the dependency array, but the logic will get more and more complicated. And you can't be sure to not introduce bugs when you change it.

 

So, let's see how we can use the rule to be confident about our code!

 

Remove the useEffect if possible

We write useEffect to run side effects in our code. But you should think about if it is useful or not. If your useEffect reacts to a state change to set another derived state, it could probably be refactored to set both states in the same place.

In our example, we react to the ++code>searchText++/code> state to fetch data and set a new state. We can remove the filter logic from our useEffect because the component is already re-rendered when ++code>setSearchText++/code> is called:

Full example

 

Now, we have two states ++code>data++/code> and ++code>searchText++/code> that are used to calculate ++code>filteredData++/code> which is not a derived state.

The search still works, and we don't re-fetch data when searching another text!

Logs of the console before and after optimizations

We improved the quality of the code and removed one dependency in our useEffect. But we still have some work to have exhaustive dependencies!

 

Declare the function inside the useEffect

If we look at the eslint warning we see:

React Hook useEffect has a missing dependency: 'fetchAndStoreData'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)ESLint

 

If we add the function to the dependency array, we have seen that it re-render and fetch the data indefinitely. But it adds another warning on the ++code>fetchAndStoreData++/code> function:

The 'fetchAndStoreData' function makes the dependencies of useEffect Hook (at line 89) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'fetchAndStoreData' definition into its own useCallback() Hook. (react-hooks/exhaustive-deps)ESLint

Note that we see this warning only if the function is in the same file as the useEffect. So you should apply this advice even if the function comes from another file.

 

In this case, we have a function that is used only in the useEffect. So, to add exhaustive dependencies, we can move ++code>fetchAndStoreData++/code> inside the useEffect. Then, eslint rule explains that we should include ++code>postId++/code> as a dependency:

Full example

 

That's all! The app is still working and the useEffect is not called at every re-render:

Logs of the console

Thanks to exhaustive dependencies we will be able to catch missing dependencies when we modify fetch logic in another feature.

This is the easiest way to solve this warning because we don't need to memoize anything.

 

Memoize the function and put it in the dependency array OF THE USEEFFECT

In some cases we can't move the function in the useEffect because it is used in several places. We need to memoize the function and include it in the dependency array.

On our app we would have:

The ++code>react-hooks/exhaustive-deps++/code> also works on useCallback and useMemo, so you will not forget to add ++code>postId++/code>!

Now, you can be confident about all the useEffect that call ++code>fetchAndStoreData++/code>!

 

What more?

To conclude, according to this post of Dan Abramov, you can disable the rule for some specific pattern when you really know what you're doing.

 

For example, if you want to have a ++code>componentDidMount++/code> behavior, you can create a ++code>useComponentDidMount++/code> custom hook that implement a useEffect with an empty dependency array where you can ignore and comment the eslint warning.

 

Here is a possible implementation of ++code>useComponentDidMount++/code>:

But, it should be rare. According to Dan Abramov in the same post, Facebook app has only ~30 warnings in a very large codebase.

Rejoins nos équipes