What does the react-hooks/exhaustive-deps rule from eslint-plugin-react-hooks actually check, and what does it cost you to silence it with an eslint-disable comment?
answer
- it diffs reads against the array
- reactive means declared in the component
- a non-literal array defeats it
- disabling suspends future checks, not just this one
- restructure so the honest array is short
basics
~20 sThe exhaustive-deps rule compares the reactive values a hook's callback reads against the dependency array you wrote, reporting missing or unnecessary entries. Disabling it freezes that comparison forever, so later edits to the callback go unchecked.
solid answer
~50 sIt is a static check on hooks that take a dependency array — `useEffect`, `useLayoutEffect`, `useCallback`, `useMemo`, `useImperativeHandle`, plus any custom hook you list in the rule's `additionalHooks` option. It reads the callback body, collects every value defined in the component that the callback references, and diffs that set against the array literal you passed, reporting a missing dependency or an unnecessary one. It is a warning in the plugin's recommended config, not an error, because a genuinely correct suppression exists. The cost of `// eslint-disable-next-line react-hooks/exhaustive-deps` is that the check is switched off permanently at that call site: the person who adds a new variable to the callback six months later gets no warning, and the hook keeps whatever stale values it captured. I treat a disable comment as needing a written reason, and usually the honest fix is restructuring — a functional state updater, moving the function inside the callback, or holding the value in a ref.
go deeper
Know what the warning is telling you: something the callback reads is not listed in the array. Adding the missing name is usually right; deleting the warning is not.
Explain the mechanics — reactive values collected from the callback body, diffed against an array literal, with setters and module-level values excluded — and name a shape the analyser cannot check.
Show the judgment: when a suppression is defensible, why it must be one line with a stated reason, and which restructurings (functional updater, move the function inside, ref for non-reactive reads) make the warning disappear honestly.
Own the policy question. Decide whether the rule is an error or a warning in CI, how the existing backlog gets cleared, and why an unread warning stream is indistinguishable from the rule being disabled.
## What the rule inspects `react-hooks/exhaustive-deps` runs on hook calls that take a callback plus a dependency array. Out of the box it recognises `useEffect`, `useLayoutEffect`, `useCallback`, `useMemo` and `useImperativeHandle`; a project's own wrapper hooks can be added through the rule's `additionalHooks` option, which takes a regular expression matching the hook names to treat the same way. For each such call it walks the callback body and collects the identifiers it references that are *reactive* — declared inside the component (props, state, values derived from them, functions defined in the body). Module-level constants, imported functions, and the setter returned by `useState` are excluded, because they cannot change between renders. It then diffs that set against the entries in the array literal and reports the difference in both directions: `React Hook useEffect has a missing dependency: 'userId'` when something is used but not listed, and an unnecessary-dependency report when something is listed but never read. ```jsx useEffect(() => { const t = setInterval(() => setCount(count + 1), 1000); return () => clearInterval(t); }, []); // warns: missing dependency 'count' ``` ## Why it is a warning, not an error In the plugin's recommended configuration `react-hooks/rules-of-hooks` is an error and `react-hooks/exhaustive-deps` is a warning. That asymmetry is deliberate. A rules-of-hooks violation is unconditionally a defect — there is no correct program that breaks call ordering. An exhaustive-deps report is a very good heuristic, but a small set of correct programs legitimately omit a dependency: a mount-only effect whose captured value is genuinely wanted at its mount-time value, or a hook whose dependency is stable by construction in a way the analyser cannot prove. ## The limits of the analysis It is purely syntactic, and there are shapes it cannot reason about: - **A non-literal array.** `useEffect(fn, deps)` where `deps` is a variable defeats it entirely — the rule reports that the dependency list is not an array literal and stops checking, because it has nothing to compare against. - **A callback that is not an inline function.** If you pass a function defined elsewhere, the rule cannot see what it reads. - **Indirection through a helper.** A function defined in the component body counts as one dependency; whether *that* function's own reads are covered depends on it being recreated each render. These are worth naming in an interview because they are the cases where a passing lint run means nothing. ## What a disable comment actually costs Suppressing the rule does not assert "this array is correct today". It asserts "never check this call site again". The realistic failure is not the moment you write it — you presumably reasoned about it — but the next edit. Someone adds a line to the callback that reads a new prop, the array is not updated, no warning appears, and the hook runs with a value captured on an earlier render. That class of bug is invisible in review because the disable comment reads as an approved decision rather than as suspended checking. So the practical discipline: a disable comment carries a one-line reason on the same line, and its scope is one call site (`eslint-disable-next-line`, never a file-level disable). If the reason cannot be written in a sentence, the code needs restructuring instead. ## The restructurings that make the warning go away honestly Most warnings are a signal that the dependency should not have been read directly: - A state value read only in order to update it becomes the functional updater form: `setCount(c => c + 1)` reads nothing, so the dependency disappears. - A function defined in the component body and used by exactly one effect can move inside the callback, which removes it from the dependency set entirely. - A value that must be current but must not re-trigger the hook can be held in a ref and read as `ref.current` inside the callback, which the rule correctly treats as non-reactive. Each of these changes the code so that the honest dependency array is the short one, rather than lying about the long one. That is the difference an interviewer is listening for: suppressing the rule negotiates with the linter, restructuring negotiates with the actual dependency. ## One organisational note Because the rule fires as a warning, teams that run ESLint with warnings unfiltered accumulate hundreds of them and stop reading the output. Either promote it to an error and fix the backlog, or fail CI on new warnings only — a permanent warning stream is functionally the same as having the rule switched off.
- Why does the rule not require the setter from useState in the dependency array?Because React guarantees the setter's identity is stable for the lifetime of the component, so including it could never cause a re-run and omitting it can never cause staleness. The rule hard-codes that knowledge. The same reasoning applies to `dispatch` from `useReducer` and to anything defined outside the component, which cannot change between renders.
- A teammate passes a variable as the dependency array to keep the code DRY. What do you tell them?That it silently turns the rule off for that call. The analyser reports that the list is not an array literal and cannot compare anything, so both missing and stale dependencies go unreported from then on. Inline literals are the price of static checking; if several hooks genuinely share a list, extracting a custom hook and registering it in `additionalHooks` keeps the checking.
- Would you promote exhaustive-deps to an error in a large codebase?Yes, once the existing backlog is cleared, because a warning nobody fails on is a rule nobody obeys. The migration path is to fix or explicitly suppress every current report first, so the rule flips to error with a clean tree — otherwise the team's first response is a file-level disable, which is worse than the warning was.
saying these in an interview costs you the question
- Says an empty dependency array means the effect has no dependencies
- Treats a disable comment as proof the array was reviewed
- Claims the rule verifies behaviour rather than comparing identifiers
- Thinks passing a variable as the deps array still gets checked
- Says listing extra dependencies is always harmless