Stephan Miller

Understanding the React exhaustive-deps Linting Warning

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. It works similar to setState. But then you have to update state based on prop, which means it’s time to try useEffect which 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.

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

Well, 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 hard to track down. This happens when a variable in the useEffect never updates. Sometimes this is what you want, but most 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 2 seconds remains 0 the whole 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 hover over the squigglies under the dependency array in the code example, you will see why lint is angry.

exhaustive-deps-warning

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 ok, but it defeats the purpose of having the useEffect in the first place. So the obvious answer is add the count variable to the dependency array. In VS Code, with the ESlint extension, and in other IDE’s 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. I think 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 make sure 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 your 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

Then 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 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 at 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 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.

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 and you will notice there is 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.

So to remove the warning, you could add the obj variable to the array, but this means the function in the useEffect will run on every render. This is also what using Quick Fix will do and not really how you want the app to function. One thing you could do is 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 the useEffect or outside of the component to remove the warning, since 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 the 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 that 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 change and the useEffect will only run when group constant changes. We could also just simply add these values to the dependency array instead of using useMemo.

Functions

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

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();
  }, [])
}

With the complexity of React hooks and the hard-to-find issues you can create if you take a wrong turn, adding the exhaustive deps lint rule along with the rule of hooks rule is a necessity. These rules will save you a lot of time and instantly tell you when you did something wrong and with the right IDE extensions, even fix the problem for you. While often the Quick Fix works, you should always examine your code to understand why there was a warning to find the best fix if you want to prevent unnecessary renders and possibly find a better solution.

This article was originally published on LogRocket

Stephan Miller

Written by

Kansas City Software Engineer and Author

Twitter | Github | LinkedIn

Updated