exhaustive-deps
linting warningWhen 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.
If you hover over the squiggles under the dependency array in the code example, you will see why lint is angry.
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:
It will also check the dependency arrays in your Hooks to ensure you get the functionality you expect from them.
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.
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.
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
.
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.
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 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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]