Fixing exhaustive-deps warning in some common use cases
In the previous article, we looked at the right mental model one needs to adopt to use the useEffect hook correctly. In this article, let’s look at how we can adapt this mental model to address some of the common use cases. This will also help you avoid the exhaustive-deps warning.
Doing something on mount
To begin with, this very idea itself is flawed in my opinion because we should not be thinking in terms of lifecycle methods in functional components.
But there could be cases where we may need to send an API request to fetch some data to show in the component. If this is not dependent on a state or a prop value, we can pass an empty array as the dependency into a useEffect hook and this API request will be sent only once. It is important to note that we are not asking React to call the useEffect hook only on mount by passing an empty array as many might think. We are simply telling React that this hook does not depend on anything so React will call this only during the first render.
But what if the API request is dependent on a state or prop value? Well, if this is the case, then we should call the API every time the state or the prop value changes. Otherwise, why should this depend on the state or prop value in the first place? If we take our pagination example in the previous article, we should send an API request every time the active-page state changes. Calling it once does not make sense. So, predictably you are going to get the exhaustive-deps warning if you pass an empty dependency array.
However, if you still have a use case for this, then you can consider replacing the state or prop value with their initial value. After all, you don’t want to sync it with changes to the state or prop value. So, why bother using it at all?
Still, if you think you have a use case, then you can use the useRef hook. Using this hook, we can make sure the API request is sent only once. You will see that also helps us avoid the exhaustive-deps warning.
Dependency loops
There could be cases where we may have to set a state with a value derived from the same state. In such cases, passing the state as a dependency could lead to an infinite loop. For example, let’s take a component that adds different prop values and displays the output on the screen.
Every time we update the prop value, the component should add the new prop value to the existing summation of prop values. We can maintain the sum in a state and add the prop value to this state whenever the prop value changes.
We can implement this using a useEffect hook. Within this hook, we can add the prop value to the existing state value and update the state with the new total. However, since we use both the state and the prop values, we will have to pass both as dependencies. Not doing so can throw the exhaustive-deps warning. But React is going to call this hook even when the state value changes. Since we update the state value within the hook, this will result in an infinite loop of state updates.
We can solve this problem by using the functional updater form of setState. The setState method can also accept a callback function to update the state. This callback function takes the existing state value as an argument and returns the new state value.
So, instead of calculating the new sum from the state value directly, we can do this within the callback function by using the existing state passed as an argument. This means that we don’t need to pass the state value as a dependency. This effectively fixes the infinite loop issue.
The useReducer method
We can also resolve this issue by using the useReducer hook instead of the useState hook. The useReducer hook works very similar to reducers in Redux. You can define the reducer and then dispatch actions to mutate the state.
We can add the prop value to the state within the reducer so that we don’t need to pass the state value as a dependency to the useEffect hook.
Functions within the component scope
When we call the functions that we have defined inside the component within useEffect, we are going to once again get the exhaustive-deps warning. So, we also need to pass the function as a dependency.
However, doing so will result in React calling the useEffect hook following every render. The reason is that during re-renders, React calls the functional component. Even though it is a component, we should not forget that it is still a function. When React calls this function to re-render the component, everything that we defined inside the function is going to be re-initialized. This is applicable to the functions within the functional component as well.
This means the function references are not going to be the same across re-renders. This leads to React calling the useEffect hook following every re-render.
One way of solving this issue is to define the function inside the useEffect hook. This removes the function from the functional component scope so we don’t need to pass this into the dependency array.
useCallback
However, this might not always be possible, especially if we need to call this function elsewhere. We can make use of the useCallback hook in such scenarios. The useCallback hook takes a function as the first argument and a dependency array as the second argument. As long as the dependencies do not change, React will make sure that the function reference does not change. This means that React will not call the useEffect hook following every re-render.
Unlearning the model that we have about the useEffect hook and learning the correct model is not an easy task. You could also be tempted to disable the exhaustive-deps rule. It took me a while to get used to this new mental model. But trust me. Once you get the hang of it, the particularities of the functional components will start to make more sense. You will also write code that doesn’t employ hacks to accommodate the class-based mental model. As you get used to this new mental model, you will also realize that this is very simpler in comparison and allows you to produce less buggy code.
Leave a Reply