Understanding the exhaustive-deps Eslint rule in React

avatar
Borislav Hadzhiev

Last updated: Apr 7, 2024
4 min

banner

# Understanding the exhaustive-deps Eslint rule in React

The "react-hooks/exhaustive-deps" rule warns us when we have a missing dependency in an effect hook.

To get rid of the warning, move the function or variable declaration inside of the useEffect hook, memoize arrays and objects that change on every render or disable the rule.

Here is an example of how the warning is caused.

App.js
import React, {useEffect, useState} from 'react'; export default function App() { const [address, setAddress] = useState({country: '', city: ''}); // ๐Ÿ‘‡๏ธ objects/arrays are different on re-renders // They are compared by reference (not by contents) const obj = {country: 'Germany', city: 'Hamburg'}; useEffect(() => { setAddress(obj); console.log('useEffect called'); // โ›”๏ธ React Hook useEffect has a missing dependency: 'obj'. // Either include it or remove the dependency array. eslintreact-hooks/exhaustive-deps }, []); return ( <div> <h1>Country: {address.country}</h1> <h1>City: {address.city}</h1> </div> ); }
The code for this article is available on GitHub

The issue is that we're making use of the obj variable inside of the useEffect hook, but we aren't including it in the dependencies array.

The most obvious solution to the error would be to add the obj variable to the dependencies array of the useEffect hook.

However, in this case, it would cause an error because objects and arrays are compared by reference in JavaScript.

The obj variable is an object with the same key-value pairs on each re-render, but it points to a different location in memory every time, so it would fail the equality check and cause an infinite re-render loop.

Arrays are also compared by reference in JavaScript.

# Disabling the Eslint rule

One way to get around the warning React Hook useEffect has a missing dependency is to disable the Eslint rule for a single line or for the entire file.

App.js
import React, {useEffect, useState} from 'react'; export default function App() { const [address, setAddress] = useState({country: '', city: ''}); // ๐Ÿ‘‡๏ธ Objects/Arrays are different on re-renders const obj = {country: 'Germany', city: 'Hamburg'}; useEffect(() => { setAddress(obj); console.log('useEffect called'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <div> <h1>Country: {address.country}</h1> <h1>City: {address.city}</h1> </div> ); }

disabling the eslint rule

The code for this article is available on GitHub

The comment above the dependencies array disables the react-hooks/exhausting-deps rule for a single line.

When the useEffect hook is passed an empty array as the second parameter, it is only called when the component mounts.

# Move the variable or function inside the useEffect hook

An alternative solution is to move the variable or function declaration inside of the useEffect hook.

App.js
import React, {useEffect, useState} from 'react'; export default function App() { const [address, setAddress] = useState({ country: '', city: '', }); useEffect(() => { // ๐Ÿ‘‡๏ธ Move object/array/function declaration // Inside of the useEffect hook const obj = {country: 'Germany', city: 'Hamburg'}; setAddress(obj); console.log('useEffect called'); }, []); return ( <div> <h1>Country: {address.country}</h1> <h1>City: {address.city}</h1> </div> ); }

move variable or function inside use effect hook

The code for this article is available on GitHub

We moved the variable declaration for the object inside of the useEffect hook.

This removes the warning because the hook no longer has a dependency on an external object.

# Move the variable or function outside of the component

Another possible solution, which can be used rarely, but is good to know about, is to move the function or variable declaration out of your component.

App.js
import React, {useEffect, useState} from 'react'; // ๐Ÿ‘‡๏ธ Move function/variable declaration outside of the component const obj = {country: 'Germany', city: 'Hamburg'}; export default function App() { const [address, setAddress] = useState({ country: '', city: '', }); useEffect(() => { setAddress(obj); console.log('useEffect called'); }, []); return ( <div> <h1>Country: {address.country}</h1> <h1>City: {address.city}</h1> </div> ); }

move variable or function outside the component

The code for this article is available on GitHub

This helps because the variable won't get recreated every time the App component is re-rendered.

The variable will point to the same location in memory on all renders, therefore useEffect doesn't need to keep track of it in its dependencies array.

# Use the useMemo hook to memoize the value

An alternative solution is to use the useMemo hook to get a memoized value.

App.js
import React, {useMemo, useEffect, useState} from 'react'; export default function App() { const [address, setAddress] = useState({country: '', city: ''}); // ๐Ÿ‘‡๏ธ Get the memoized value const obj = useMemo(() => { return {country: 'Germany', city: 'Hamburg'}; }, []); useEffect(() => { setAddress(obj); console.log('useEffect called'); // ๐Ÿ‘‡๏ธ Safely include in the dependencies array }, [obj]); return ( <div> <h1>Country: {address.country}</h1> <h1>City: {address.city}</h1> </div> ); }
The code for this article is available on GitHub

We used the useMemo hook to get a memoized value that doesn't change between renders.

The useMemo hook takes a function that returns a value to be memoized and a dependencies array as parameters. The hook will only recompute the memoized value if one of the dependencies has changed.

# Use the useCallback hook to memoize the function

If you're working with a function, you would use the useCallback hook to get a memoized callback that doesn't change between renders.

App.js
import React, {useMemo, useEffect, useState, useCallback} from 'react'; export default function App() { const [address, setAddress] = useState({country: '', city: ''}); // ๐Ÿ‘‡๏ธ Get the memoized callback const sum = useCallback((a, b) => { return a + b; }, []); // ๐Ÿ‘‡๏ธ Get the memoized value const obj = useMemo(() => { return {country: 'Germany', city: 'Santiago'}; }, []); useEffect(() => { setAddress(obj); console.log('useEffect called'); console.log(sum(100, 100)); // ๐Ÿ‘‡๏ธ Safely include in the dependencies array }, [obj, sum]); return ( <div> <h1>Country: {address.country}</h1> <h1>City: {address.city}</h1> </div> ); }
The code for this article is available on GitHub

The useCallback hook takes an inline callback function and a dependencies array and returns a memoized version of the callback that only changes if one of the dependencies has changed.

# Alternatively, disable the Eslint rule

If none of the suggestions worked for your use case, you can always silence the warning with a comment.

App.js
import React, {useEffect, useState} from 'react'; export default function App() { const [address, setAddress] = useState({country: '', city: ''}); const obj = {country: 'Chile', city: 'Santiago'}; useEffect(() => { setAddress(obj); console.log('useEffect called'); // ๐Ÿ‘‡๏ธ Disable the rule for a single line // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <div> <h1>Country: {address.country}</h1> <h1>City: {address.city}</h1> </div> ); }

disabling the eslint rule

The code for this article is available on GitHub
I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.

Copyright ยฉ 2024 Borislav Hadzhiev