Skip to content

useExhaustiveDependencies

biome.json
{
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": "error"
}
}
}
}

Enforce correct dependency usage within React hooks.

React components have access to various hooks that can perform various actions like querying and updating state.

For hooks that trigger whenever a variable changes (such as useEffect and useMemo), React relies on the hook’s listed dependencies array to determine when to re-compute Effects and re-render the page.

This can lead to unexpected behavior when dependencies are incorrectly specified:

function ticker() {
const [count, setCount] = useState(0);
/** Increment the count once per second. */
function onTick() {
setCount(count + 1);
}
// React _thinks_ this code doesn't depend on anything else, so
// it will only use the _initial_ version of `onTick` when rendering the component.
// As a result, our normally-dynamic counter will always display 1!
// This is referred to as a "stale closure", and is a common pitfall for beginners.
useEffect(() => {
const id = setInterval(onTick, 1000);
return () => clearInterval(id);
}, []);
return <h1>Counter: {count}</h1>;
}
function apples() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("We have 0 apples!");
// React _thinks_ this code depends on BOTH `count` and `message`, and will re-run the hook whenever
// `message` is changed despite it not actually being used inside the closure.
// In fact, this will create an infinite loop due to our hook updating `message` and triggering itself again!
useEffect(() => {
setMessage(`We have ${count} apples!`)
}, [count, message]);
}

This rule attempts to prevent such issues by diagnosing potentially incorrect or invalid usages of hook dependencies.

By default, the following hooks (and their Preact counterparts) will have their arguments checked by this rule:

  • useEffect
  • useLayoutEffect
  • useInsertionEffect
  • useCallback
  • useMemo
  • useImperativeHandle

When a hook is known to have a stable return value (one whose identity doesn’t change across invocations), that value doesn’t need to and should not be specified as a dependency. For example, setters returned by React’s useState hook will not change throughout the lifetime of a program and should therefore be omitted.

By default, the following hooks are considered to have stable return values:

  • useState (index 1)
  • useReducer (index 1)
  • useTransition (index 1)
  • useRef
  • useEffectEvent

If you want to add custom hooks to the rule’s diagnostics or specify your own functions with stable results, see the options section for more information.

import { useEffect } from "react";
function component() {
let a = 1;
useEffect(() => {
console.log(a);
}, []);
}
code-block.js:5:3 lint/correctness/useExhaustiveDependencies  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This hook does not specify its dependency on a.

3 │ function component() {
4 │ let a = 1;
> 5 │ useEffect(() => {
^^^^^^^^^
6 │ console.log(a);
7 │ }, []);

This dependency is being used here, but is not specified in the hook dependency list.

4 │ let a = 1;
5 │ useEffect(() => {
> 6 │ console.log(a);
^
7 │ }, []);
8 │ }

React relies on hook dependencies to determine when to re-compute Effects.
Failing to specify dependencies can result in Effects not updating correctly when state changes.
These “stale closures” are a common source of surprising bugs.

Either include it or remove the dependency array.

Unsafe fix: Add the missing dependency to the list.

7 │ ··},·[a]);
+
import { useEffect } from "react";
function badComponent() {
let a = 1;
useEffect(() => {
console.log(a);
}, "not an array");
}
code-block.js:7:6 lint/correctness/useExhaustiveDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This dependencies list is not an array literal.

5 │ useEffect(() => {
6 │ console.log(a);
> 7 │ }, “not an array”);
^^^^^^^^^^^^^^
8 │ }
9 │

Biome can’t statically verify whether you’ve passed the correct dependencies.
Replace the argument with an array literal and list your dependencies within it.

import { useEffect } from "react";
function component() {
let unused = 1;
useEffect(() => {}, [unused]);
}
code-block.js:5:5 lint/correctness/useExhaustiveDependencies  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This hook specifies more dependencies than necessary: unused.

3 │ function component() {
4 │ let unused = 1;
> 5 │ useEffect(() => {}, [unused]);
^^^^^^^^^
6 │ }
7 │

This dependency can be removed from the list.

3 │ function component() {
4 │ let unused = 1;
> 5 │ useEffect(() => {}, [unused]);
^^^^^^
6 │ }
7 │

React relies on hook dependencies to determine when to re-compute Effects.
Specifying more dependencies than required can lead to unnecessary re-rendering
and degraded performance.

Unsafe fix: Remove the extra dependencies from the list.

5 │ ····useEffect(()·=>·{},·[unused]);
------
import { useEffect, useState } from "react";
function component() {
const [name, setName] = useState();
useEffect(() => {
console.log(name);
setName("i never change and don't need to be here");
}, [name, setName]);
}
code-block.js:5:3 lint/correctness/useExhaustiveDependencies  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This hook specifies more dependencies than necessary: setName.

3 │ function component() {
4 │ const [name, setName] = useState();
> 5 │ useEffect(() => {
^^^^^^^^^
6 │ console.log(name);
7 │ setName(“i never change and don’t need to be here”);

This dependency can be removed from the list.

6 │ console.log(name);
7 │ setName(“i never change and don’t need to be here”);
> 8 │ }, [name, setName]);
^^^^^^^
9 │ }
10 │

React relies on hook dependencies to determine when to re-compute Effects.
Specifying more dependencies than required can lead to unnecessary re-rendering
and degraded performance.

Unsafe fix: Remove the extra dependencies from the list.

8 │ ··},·[name,·setName]);
---------
import { useEffect, useState } from "react";
function component() {
const name = "foo"
// name doesn't change, so specifying it is redundant
useEffect(() => {
console.log(name);
}, [name]);
}
code-block.js:6:3 lint/correctness/useExhaustiveDependencies  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This hook specifies more dependencies than necessary: name.

4 │ const name = “foo”
5 │ // name doesn’t change, so specifying it is redundant
> 6 │ useEffect(() => {
^^^^^^^^^
7 │ console.log(name);
8 │ }, [name]);

This dependency can be removed from the list.

6 │ useEffect(() => {
7 │ console.log(name);
> 8 │ }, [name]);
^^^^
9 │ }
10 │

React relies on hook dependencies to determine when to re-compute Effects.
Specifying more dependencies than required can lead to unnecessary re-rendering
and degraded performance.

Unsafe fix: Remove the extra dependencies from the list.

8 │ ··},·[name]);
----
import { useEffect } from "react";
function component() {
let a = 1;
const b = a + 1;
useEffect(() => {
console.log(b);
}, []);
}
code-block.js:6:3 lint/correctness/useExhaustiveDependencies  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This hook does not specify its dependency on b.

4 │ let a = 1;
5 │ const b = a + 1;
> 6 │ useEffect(() => {
^^^^^^^^^
7 │ console.log(b);
8 │ }, []);

This dependency is being used here, but is not specified in the hook dependency list.

5 │ const b = a + 1;
6 │ useEffect(() => {
> 7 │ console.log(b);
^
8 │ }, []);
9 │ }

React relies on hook dependencies to determine when to re-compute Effects.
Failing to specify dependencies can result in Effects not updating correctly when state changes.
These “stale closures” are a common source of surprising bugs.

Either include it or remove the dependency array.

Unsafe fix: Add the missing dependency to the list.

8 │ ··},·[b]);
+
import { useEffect } from "react";
function component() {
let a = 1;
useEffect(() => {
console.log(a);
}, [a]);
}
import { useEffect } from "react";
function component() {
const SECONDS_PER_DAY = 60 * 60 * 24;
useEffect(() => {
console.log(SECONDS_PER_DAY);
});
}
import { useEffect, useState } from "react";
function component() {
const [name, setName] = useState();
useEffect(() => {
console.log(name);
setName("");
}, [name]);
}

Hooks not imported from React are ignored by default (unless specified inside rule options)

import type { EffectCallback, DependencyList } from "react";
// custom useEffect function
declare function useEffect(cb: EffectCallback, deps?: DependencyList): void;
function component() {
let name = "John Doe";
useEffect(() => {
console.log(name);
}, []);
}

Sometimes you may wish to ignore a diagnostic about a specific dependency without disabling all linting for that hook. To do so, you may specify the name of a specific dependency between parentheses, like this:

import { useEffect } from "react";
function component() {
let a = 1;
// biome-ignore lint/correctness/useExhaustiveDependencies(a): suppress dependency a
useEffect(() => {
console.log(a);
}, []);
}

If you wish to ignore multiple dependencies, you can add multiple comments and add a reason for each:

import { useEffect } from "react";
function component() {
let a = 1;
let b = 1;
// biome-ignore lint/correctness/useExhaustiveDependencies(a): suppress dependency a
// biome-ignore lint/correctness/useExhaustiveDependencies(b): suppress dependency b
useEffect(() => {
console.log(a, b);
}, []);
}

Allows specifying custom hooks (from libraries or internal projects) whose dependencies should be checked and/or which are known to have stable return values.

For every hook whose dependencies you want validated, you must specify the index of both the closure using the dependencies and the dependencies array to validate it against.

biome.json
{
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": {
"options": {
"hooks": [
{
"name": "useLocation",
"closureIndex": 0,
"dependenciesIndex": 1
},
{
"name": "useQuery",
"closureIndex": 2,
"dependenciesIndex": 0
}
]
}
}
}
}
}
}

This would enable checks on the following code snippets:

function Foo() {
let stateVar = 1;
useLocation(() => {console.log(stateVar)}, []);
}
code-block.js:3:3 lint/correctness/useExhaustiveDependencies  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This hook does not specify its dependency on stateVar.

1 │ function Foo() {
2 │ let stateVar = 1;
> 3 │ useLocation(() => {console.log(stateVar)}, []);
^^^^^^^^^^^
4 │ }
5 │

This dependency is being used here, but is not specified in the hook dependency list.

1 │ function Foo() {
2 │ let stateVar = 1;
> 3 │ useLocation(() => {console.log(stateVar)}, []);
^^^^^^^^
4 │ }
5 │

React relies on hook dependencies to determine when to re-compute Effects.
Failing to specify dependencies can result in Effects not updating correctly when state changes.
These “stale closures” are a common source of surprising bugs.

Either include it or remove the dependency array.

Unsafe fix: Add the missing dependency to the list.

3 │ ··useLocation(()·=>·{console.log(stateVar)},·[stateVar]);
++++++++
function Foo() {
let stateVar = 1;
useQuery([stateVar], "smthng", () => {console.log(stateVar)});
}

As previously discussed, the lint rule takes into account so-called stable results and will ensure any such variables are not specified as dependencies.

You can specify custom functions as returning stable results in one of four ways:

  1. "stableResult": true — marks the return value as stable. An example of a React hook that would be configured like this is useRef().
  2. "stableResult": [1] — expects the return value to be an array and marks the given indices as stable. An example of a React hook that would be configured like this is useState().
  3. "stableResult": 1 — shorthand for option 2 ("stableResult": [1]). Useful for hooks that only have a single stable return.
  4. "stableResult": ["setValue"] — expects the return value to be an object and marks the properties with the given keys as stable.
biome.json
{
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": {
"options": {
"hooks": [
{
"name": "useDispatch",
"stableResult": true
}
]
}
}
}
}
}
}

With this configuration, the following is valid:

const dispatch = useDispatch();
// No need to list `dispatch` as dependency since it doesn't change
const doAction = useCallback(() => dispatch(someAction()), []);

If set to false, the rule will not trigger diagnostics for unused dependencies passed to hooks that do not use them.

Default: true

biome.json
{
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": {
"options": {
"reportUnnecessaryDependencies": false
}
}
}
}
}
}
import { useEffect } from "react";
function Foo() {
let stateVar = 1;
// not used but still OK
useEffect(() => {}, [stateVar]);
}

If enabled, the rule will also trigger diagnostics for hooks that lack dependency arrays altogether, requiring any hooks lacking dependencies to explicitly specify an empty array.

Default: false

biome.json
{
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": {
"options": {
"reportMissingDependenciesArray": true
}
}
}
}
}
}
function noArrayYesProblem() {
let stateVar = 1;
React.useEffect(() => {});
}
code-block.jsx:3:9 lint/correctness/useExhaustiveDependencies ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This hook does not have a dependencies array.

1 │ function noArrayYesProblem() {
2 │ let stateVar = 1;
> 3 │ React.useEffect(() => {});
^^^^^^^^^
4 │ }
5 │

React relies on hook dependencies to determine when to re-compute Effects.
Add an explicit array (i.e. []) and list the callback’s dependencies inside it.