Stephan Miller Stephan is a full-stack web and mobile developer with over 16 years of experience.

Understanding the React exhaustive-deps linting warning

6 min read 1695

React Logo

Table of Contents

When you move from using class components to functional components in React, it can be a bumpy ride. You have to learn how to use React Hooks. Most likely the first one you learned is useState. No big issues there because it works similarly to setState.

But then you have to update state based on prop, which means it’s time to try useEffect. This can seem simple, too, at first: you read about the dependency array and it makes sense. But it is also pretty easy to make a wrong turn when using useEffect. Let’s look at an example.

const App = () => {
   const [randomNumber, setRandomNumber] = useState();
   React.useEffect(() => {
     setRandomNumber(Math.random());
   }, [randomNumber]);
 }

The mistake in the code above is pretty obvious, and you can see the results here. The code above creates an endless loop. But another way you can make a mistake with useEffect is by creating a stale closure, which can be harder to spot and causes issues that could be difficult to track down.

This happens when a variable in useEffect never updates. Sometimes this is what you want, but most of the time, you don’t. Here is an example of a stale closure.

const App() {
  const [count, setCount] = useState(0);
  useEffect(function () {
    setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
  }, []);

  return (
    <div className="App">
      {count}
      <button onClick={() => setCount(count + 1)}>Increase</button>
    </div>
  );
}

You can see this code running here. The count variable that renders in the component updates when you click the button, but the value that gets logged in the useEffect every two seconds remains 0 the entire time. We were expecting this, but as your code gets more complex, it may be harder to find the issue. Fortunately, you can use an ESlint plugin to find it for you, before it becomes a bug.

What is the exhaustive deps lint rule?

If you hover over the squiggles under the dependency array in the code example, you will see why lint is angry.

Missing Dependency

You will see that lint gives you a couple of options: either adding count to the dependency array or removing the dependency array altogether. If you remove the dependency array, the function inside will run on every render. I guess this is fine, but it defeats the purpose of having useEffect in the first place.

The obvious answer is to add the count variable to the dependency array. In VS Code, with the ESlint extension, and in other IDEs with similar functionality, you can click on the Quick Fix link and count will be added to the dependency array for you.

Linting is very controversial. Most developers think it should be done, but they widely disagree on how it should be done. Many of the rules like those about indention, the spacing of curly brackets, and others are more about readability than they are about ensuring good code.

But eslint-plugin-react-hooks can keep you from making mistakes with React Hooks that can be hard to track down and debug. This ESlint plugin will ensure you are following the rules of Hooks in your React code, which are:

  • Only call Hooks at the top level
  • Only call Hooks from React functions

It will also check the dependency arrays in your Hooks to ensure you get the functionality you expect from them.

How to add this rule to React projects

If you are using Create React App, the ESlint plugin for React Hooks is already included by default. To add it to an existing project, just install it with npm or yarn.

npm install eslint-plugin-react-hooks --save-dev
yarn add eslint-plugin-react-hooks --dev

Next, add the following to your ESlint configuration:

// ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // For checking rules of hooks
    "react-hooks/exhaustive-deps": "warn" // For checking hook dependencies 
  }
}

You will also want to add the ESlint extension to your IDE to make it easier to correct the warnings. For VS Code, you can use this extension and for Atom, you can use this one.

How to fix exhaustive deps warnings

Once you have the exhaustive deps rule in your ESlint configuration, you may run into some warnings that might require more thinking than our first example did. You can find a long list of comments on this rule on GitHub where developers using the rule weren’t quite sure why they were getting a warning or how to fix it.

The first exhaustive deps warning we got was because a single primitive variable was missing in the dependency array.

const App() {
  const [count, setCount] = useState(0);
  useEffect(function () {
    console.log(`Count is: ${count}`);
  }, []);

  return (
    <div className="App">
      {count}
      <button onClick={() => setCount(count + 1)}>Increase</button>
    </div>
  );
}

It is pretty simple to see why there is a warning and why the fix you get by clicking Quick Fix in your IDE will work. Adding the count variable will fix the problem and not cause any weird issues. But sometimes, the suggested solution has to be examined before you use it.

Using objects and arrays

export default function App() {
  const [address, setAddress] = useState({ country: "", city: "" });

  const obj = { country: "US", city: "New York" };

  useEffect(() => {
    setAddress(obj);
  }, []);

  return (
    <div>
      <h1>Country: {address.country}</h1>
      <h1>City: {address.city}</h1>
    </div>
  );
}

You can see this code live here. Notice there is a warning about the dependency array needing the obj variable. Weird! It is always the same value, so why would this happen?

Objects and arrays in JavaScript are compared by reference, not by value. Each time the component renders, this object has the same value but a different reference. The same thing will happen if the variable was an array.

To remove the warning, you could add the obj variable to the array, but this means the function in useEffect will run on every render. This is also what clicking Quick Fix will do and not really how you want the app to function. One solution is to use an attribute of the object in the dependency array.

  useEffect(() => {
    setAddress(obj);
  }, [obj.city]);

Another option is to use the useMemo Hook to get a memoized value of the object.



const obj = useMemo(() => {
    return { country: 'US', city: 'New York' };
  }, []);

You can also move the obj variable either into useEffect or outside of the component to remove the warning, because in these locations, it won’t be recreated on every render.

// Move it here
// const obj = { country: "US", city: "New York" };
export default function App() {
  const [address, setAddress] = useState({ country: "", city: "" });

  const obj = { country: "US", city: "New York" };

  useEffect(() => {
    // Or here
    // const obj = { country: "US", city: "New York" };

    setAddress(obj);
  }, []);

  return (
    <div>
      <h1>Country: {address.country}</h1>
      <h1>City: {address.city}</h1>
    </div>
  );
}

The above examples are contrived to show possible ways to fix a dependency array warning when it doesn’t seem to make sense. Here is an example you might want to look out for in your code.

You do want to fix the warning without disabling the lint rule, but, like in the above examples, it will cause the function in useEffect to run unnecessarily to use the lint suggestions. In this example, we have props that are being passed into a component:

import { getMembers } from '../api';
import Members from '../components/Members';

const Group = ({ group }) => {
  const [members, setMembers] = useState(null);

  useEffect(() => {
    getMembers(group.id).then(setMembers);
  }, [group]);

  return <Members group={group} members={members} />
}

If the group prop is an object, React will check if the current render points to the same object in the previous render. So, even if the object is the same, if a new object was created for the subsequent render, useEffect will run.

We can fix this problem by checking for a specific attribute in the group object that we know will change.

useEffect(() => {
    getMembers(group.id).then(setMembers);
  }, [group.id]);

You can also use the useMemo Hook if any of the values could change.

import { getMembers, getRelatedNames } from '../api';
import Members from '../components/Members';

const Group = ({ id, name }) => {
  const group = useMemo(() => () => return { id, name }, [
    id,
    name,
  ]);
  const [members, setMembers] = useState(null);
  const [relatedNames, setRelatedNames] = useState(null);

  useEffect(() => {
    getMembers(id).then(setMembers);
    getRelatedNames(names).then(setRelatedNames);
  }, []);

  return <Members group={group} members={members} />
}

Here, the group variable will update when either the id or name value changes, and useEffect will only run when group constant changes. We could also simply add these values to the dependency array instead of using useMemo.

Dealing with missing functions

There will be times when the lint warning tells you that a function is missing in an array. This will happen any time it could potentially close over state. Here is an example.


More great articles from LogRocket:


const App = ({ data }) => {
  const logData = () => {
    console.log(data);
  }

  useEffect(() => {
    logData();
  }, [])
}

This code will have a lint warning you can see here, suggesting you add logData to the dependency array. This is because it uses the data prop, which could change. To fix it, you could either follow the suggestion and add it to the dependency array, or, do this:

const App = ({ data }) => {
  useEffect(() => {
    const logData = () => {
      console.log(data);
    }

    logData();
  }, [])
}

Conclusion

With the complexity of React Hooks and the hard-to-find issues, you can create if you take a wrong turn developing your app, adding the exhaustive deps lint rule along with the rule of Hooks rule is a necessity. These rules will save you time and instantly tell you when you did something wrong.

With the right IDE extensions, even fix the problem for you. While often the “Quick Fix” solution works, you should always examine your code to understand why there was a warning. This way, you’ll find the best fix if you want to prevent unnecessary renders and possibly find an even better solution.

LogRocket: Full visibility into your production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?

Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.

No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Stephan Miller Stephan is a full-stack web and mobile developer with over 16 years of experience.

Leave a Reply