All posts
reacthooks

Fixing the useEffect Missing Dependency Warning

Why React's exhaustive-deps warning fires for useEffect, when to actually fix it versus suppress it, and the right patterns for each.

SR

Suhail Roushan

August 6, 2026

·
5 min read
·
0 views

The react-hooks/exhaustive-deps warning — "React Hook useEffect has a missing dependency" — is ESLint catching a real category of bug: an effect that reads a value from its enclosing scope without listing it as a dependency will use a stale, captured version of that value instead of the current one, and the warning exists specifically to catch that before it ships as a subtle bug.

This warning means your useEffect callback references a variable from the component's render scope (props, state, or a function defined in the component) that isn't included in the effect's dependency array — the effect will only re-run when the listed dependencies change, so an unlisted but referenced value can silently go stale.

Why This Warning Happens

Each render of a function component creates a new closure over that render's props and state. A useEffect callback captures whatever values existed at the time it was created, and only re-runs (creating a new closure with fresh values) when a listed dependency changes between renders. If the callback references a value not in the dependency array, the effect can keep using the value from whenever it last re-ran, even after that value has since changed in newer renders.

Reproducing the Warning

A stale closure bug caught by the warning:

function SearchResults({ query }: { query: string }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchResults(query).then(setResults);
  }, []); // Warning: React Hook useEffect has a missing dependency: 'query'
  // Bug: effect only runs once on mount, never re-fetching when `query` changes

  return <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>;
}

The effect references query but the empty dependency array means it only runs once — the warning is correctly flagging that results will never update when query changes, even though it visually looks like it should.

Core Concepts Behind This Warning

The dependency array should reflect every reactive value the effect actually reads, not be curated down to "what you think should trigger a re-run" — the linter is checking for consistency between what the effect reads and what it's listed as depending on, not enforcing your intended behavior, which is why suppressing it without understanding the effect's actual data flow is risky.

Not every suggested fix is "add the missing dependency" — sometimes the effect's actual intent means it shouldn't depend on that value at all, in which case restructuring (using a ref for values that shouldn't trigger re-runs, or deriving the value differently) is the more correct fix than either blindly adding the dependency or suppressing the warning.

Functions and objects defined inside the component body are new references every render, so including them as dependencies (correctly, per the warning) can cause an effect to re-run every render unless they're memoized with useCallback/useMemo — this is often what the warning is really pointing you toward: either wrap the function reference in useCallback, or move it outside the effect entirely.

Suppressing the warning with an eslint-disable comment should be a deliberate, justified exception, not a default response — it's appropriate for genuinely rare cases (an intentional "run once on mount regardless of prop changes" effect with a clear comment explaining why), not a general way to silence warnings you don't want to think through.

Fixing the useEffect Missing Dependency Warning

Fix 1 (usual case): Add the missing dependency so the effect correctly re-runs when the value changes:

useEffect(() => {
  fetchResults(query).then(setResults);
}, [query]); // now correctly re-fetches when query changes

Fix 2: When a function dependency causes unwanted re-runs due to a new reference every render, memoize it with useCallback, or move it outside the component if it doesn't need render-scope values:

const fetchResults = useCallback(async (q: string) => {
  return api.search(q);
}, []); // stable reference across renders

useEffect(() => {
  fetchResults(query).then(setResults);
}, [query, fetchResults]);

Fix 3: For values that genuinely shouldn't trigger a re-run but are still needed inside the effect (like reading the latest value without reacting to its changes), use a ref:

const latestQueryRef = useRef(query);
useEffect(() => {
  latestQueryRef.current = query;
}, [query]);

useEffect(() => {
  const interval = setInterval(() => {
    console.log("Current query:", latestQueryRef.current); // reads latest without re-running interval setup
  }, 5000);
  return () => clearInterval(interval);
}, []); // intentionally empty — interval setup shouldn't restart on query changes

Is It Ever Correct to Suppress This Warning?

Rarely, but yes — a genuine "run once on mount" effect that deliberately shouldn't react to a value's later changes (initializing a third-party library instance, for example) is a legitimate case, but should be accompanied by a comment explaining the reasoning and ideally a ref-based pattern like the one above rather than a bare suppression comment, so a future reader understands the intent wasn't just an oversight.

Preventing Stale Closure Bugs From useEffect in Production

Treat the exhaustive-deps warning as a real signal every time, resolving it deliberately (adding the dependency, memoizing a function, or moving to a ref pattern) rather than defaulting to suppression. Keep effect callbacks focused on a single clear responsibility, since effects with many dependencies and complex logic are harder to reason about correctly and more likely to have a dependency mistake slip through even with the linter's help.

If you're facing this warning, resist disabling it reflexively — work through what the effect should actually depend on first, since the fix (add dependency, memoize, or use a ref) depends entirely on the effect's real intent, not a one-size-fits-all answer.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch