src/content/docs/linter/rules/use-exhaustive-dependencies.mdx
import { Tabs, TabItem } from '@astrojs/starlight/components';
<Tabs> <TabItem label="JSX and TSX" icon="seti:javascript"> ## Summary - Rule available since: `v1.0.0` - Diagnostic Category: [`lint/correctness/useExhaustiveDependencies`](/reference/diagnostics#diagnostic-category) - This rule is **recommended**, meaning it is enabled by default. - This rule has an [**unsafe**](/linter/#unsafe-fixes) fix. - The default severity of this rule is [**error**](/reference/diagnostics#error). - This rule belongs to the following domains: - [`react`](/linter/domains#react) - [`next`](/linter/domains#next) - Sources: - Same as [`react-hooks/exhaustive-deps`](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md){
"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:
useEffectuseLayoutEffectuseInsertionEffectuseCallbackuseMemouseImperativeHandleWhen 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)useRefuseEffectEventIf 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);
}, []);
}
import { useEffect } from "react";
function badComponent() {
let a = 1;
useEffect(() => {
console.log(a);
}, "not an array");
}
import { useEffect } from "react";
function component() {
let unused = 1;
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]);
}
import { useEffect, useState } from "react";
function component() {
const name = "foo"
// name doesn't change, so specifying it is redundant
useEffect(() => {
console.log(name);
}, [name]);
}
import { useEffect } from "react";
function component() {
let a = 1;
const b = a + 1;
useEffect(() => {
console.log(b);
}, []);
}
import { useCallback } from "react";
function component() {
const Component = () => null;
const render = useCallback(() => <Component />, []);
}
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);
}, []);
}
:::caution Mismatching code & dependencies has a very high risk of creating bugs in your components. By suppressing the linter, you “lie” to React about the values your Effect depends on, so prefer changing the code over suppressing the rule where possible. :::
hooksAllows 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.
{
"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)}, []);
}
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:
"stableResult": true -- marks the return value as stable. An example
of a React hook that would be configured like this is useRef()."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()."stableResult": 1 -- shorthand for option 2 ("stableResult": [1]).
Useful for hooks that only have a single stable return."stableResult": ["setValue"] -- expects the return value to be an
object and marks the properties with the given keys as stable.{
"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()), []);
reportUnnecessaryDependenciesIf set to false, the rule will not trigger diagnostics for unused dependencies passed to hooks that do not use them.
:::caution Over-specifying dependencies can reduce application performance or even cause infinite loops, so caution is advised. :::
Default: true
{
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": {
"options": {
"reportUnnecessaryDependencies": false
}
}
}
}
}
}
import { useEffect } from "react";
function Foo() {
let stateVar = 1;
// not used but still OK
useEffect(() => {}, [stateVar]);
}
reportMissingDependenciesArrayIf 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
{
"linter": {
"rules": {
"correctness": {
"useExhaustiveDependencies": {
"options": {
"reportMissingDependenciesArray": true
}
}
}
}
}
}
function noArrayYesProblem() {
let stateVar = 1;
React.useEffect(() => {});
}