react-hooks/exhaustive-deps

Require the dependency array of a hook to list every value its callback reads

warn by defaultfixable

Also called react_hooks/exhaustive-deps, react/exhaustive-deps

React runs an effect again, or recomputes a memoized value, only when something in the dependency array changes. If the callback reads a prop or a state value that isn't in the array, it keeps the value from an earlier render. The effect doesn't run again when the prop changes, and the screen shows old data.

The rule checks useEffect, useLayoutEffect, useMemo, useCallback, and useImperativeHandle. It also reports dependencies that do nothing, dependency lists it can't read, and an async function passed to an effect. You don't have to list values that never change between renders: a setState function, a dispatch from useReducer, a ref from useRef, and anything declared outside the component.

This rule is on by default as a warning if your package.json depends on react.

Most reports come with a suggestion that rewrites the array. --fix and --fix-suggestions don't apply it, because a new dependency changes when the effect runs. --fix-dangerously does.

Configure#

On by default as a warning. To turn it off:

bunfig.toml
[lint.rules]
"react-hooks/exhaustive-deps" = "off" # or "error"

Every option, set to its default. The ones that are commented out are unset by default.

bunfig.toml
[lint.rules]
"react-hooks/exhaustive-deps" = ["warn", {
  # additionalHooks = <string>,
  requireExplicitEffectDeps = false,
  enableDangerousAutofixThisMayCauseInfiniteLoops = false,
  experimental_autoDependenciesHooks = [], # list of strings
}]

Examples#

example.jsx1 problem
function Profile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    loadUser(userId).then(setUser);
  }, []); // keeps showing the first user when userId changes

  return <h1>{user?.name}</h1>;
}
example.jsx1 problem
function Profile({ userId }) {
  useEffect(async () => {
    await track(userId);
  }, [userId]);
}
example.jsxNo problems
function Profile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    loadUser(userId).then(setUser);
  }, [userId]);

  return <h1>{user?.name}</h1>;
}
example.jsxNo problems
function Profile({ userId }) {
  useEffect(() => {
    async function send() {
      await track(userId);
    }
    send();
  }, [userId]);
}

An effect without an array runs after every render, so nothing can be missing:

example.jsxNo problems
function Title({ text }) {
  useEffect(() => {
    document.title = text;
  });
}

Options#

OptionDefaultWhat it does
additionalHooksA regular expression that matches the names of your own hooks that take a callback and a dependency array.
requireExplicitEffectDepsfalseReport an effect that has no second argument. Write undefined to say "after every render" on purpose.
enableDangerousAutofixThisMayCauseInfiniteLoopsfalseLet --fix apply the suggestions. A new dependency can make an effect run in a loop, so read the result.
experimental_autoDependenciesHooks[]Names of hooks that get their dependencies from a compiler when you pass null.
example.jsxwith { "additionalHooks": "^useDebouncedEffect$" }1 problem
function Search({ query }) {
  useDebouncedEffect(() => {
    search(query);
  }, []);
}
example.jsxwith { "requireExplicitEffectDeps": true }1 problem
function Title({ text }) {
  useEffect(() => {
    document.title = text;
  });
}