Fix 'React Hook useEffect has a missing dependency' Warning That Causes Silent Bugs

intermediateโš›๏ธ React2026-05-14| React 16.8+, any OS, ESLint with eslint-plugin-react-hooks

Error Message

React Hook useEffect has a missing dependency: 'someVariable'. Either include it or remove the dependency array. react-hooks/exhaustive-deps
#react#useEffect#eslint#hooks#dependency#exhaustive-deps

The Warning Developers Love to Ignore

You've seen it a hundred times:

React Hook useEffect has a missing dependency: 'someVariable'. Either include it or remove the dependency array.  react-hooks/exhaustive-deps

The usual response? Slap an // eslint-disable-next-line above it and move on. Bad idea. This warning exists because missing dependencies cause real, subtle bugs โ€” the kind that only surface in production after a specific sequence of user interactions, and take hours to trace back to a stale closure.

A Concrete Bug Scenario

Here's a bug pattern I've seen in at least a dozen codebases:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(data => setUser(data));
  }, []); // โ† ESLint warns: 'userId' is missing
}

Looks fine, right? Now imagine the parent passes a different userId โ€” say, when a user clicks to view another profile. The effect never re-runs. Old data stays on screen. The component silently shows information for the wrong person.

ESLint caught this. The empty array [] told React "run this once, never again" โ€” but the effect clearly depends on userId to do its job.

Why the Dependency Array Exists

React re-runs a useEffect whenever any value in its dependency array changes. Leave out a value the effect actually uses, and React never knows to re-run. The effect captures a stale snapshot of that variable and never updates โ€” a classic closure bug.

The react-hooks/exhaustive-deps ESLint rule statically analyzes your effect body and flags every variable that belongs in the array. It's annoying. It's also almost always right.

The Three Fixes

Fix 1: Add the Missing Dependency

Nine times out of ten, the answer is just: add it.

// Before (buggy)
useEffect(() => {
  fetchUser(userId).then(data => setUser(data));
}, []);

// After (correct)
useEffect(() => {
  fetchUser(userId).then(data => setUser(data));
}, [userId]);

Now whenever userId changes, the effect re-fetches fresh data. That's the behavior you actually want.

Fix 2: Functions as Dependencies โ€” Use useCallback

Here's where it gets messier. Functions defined inside a component body are recreated on every render. Add one to your dependency array without stabilizing it first, and you've got an infinite loop.

// Problem: fetchData is a new reference every render โ†’ infinite loop
function SearchPage({ query }) {
  const fetchData = () => fetch(`/api/search?q=${query}`);

  useEffect(() => {
    fetchData().then(/* ... */);
  }, [fetchData]); // different fetchData every render!
}

Wrap the function in useCallback so it only changes when its own dependencies change:

function SearchPage({ query }) {
  const fetchData = useCallback(() => {
    return fetch(`/api/search?q=${query}`);
  }, [query]); // stable reference; only updates when query does

  useEffect(() => {
    fetchData().then(/* ... */);
  }, [fetchData]); // now safe
}

Fix 3: Move the Function Inside the Effect

If that function is only used by one effect, skip useCallback entirely and define it inside:

useEffect(() => {
  const fetchData = () => fetch(`/api/search?q=${query}`);

  fetchData().then(data => setResults(data));
}, [query]); // just query โ€” clean and obvious

Less indirection. Easier to read. Usually the right call.

When an Empty Array Is Actually Correct

Sometimes you genuinely want mount-only behavior โ€” setting up a WebSocket, initializing a third-party SDK, registering a global event listener. If the effect has no reactive dependencies, [] is correct:

useEffect(() => {
  const socket = io('wss://example.com');
  socket.on('message', handleMessage);
  return () => socket.disconnect();
}, []); // correct: connect once, disconnect on unmount

The catch: if handleMessage reads state or props, you still have a stale closure problem. Use a ref to give the handler access to fresh values without re-creating the socket:

const handleMessageRef = useRef(handleMessage);
useEffect(() => {
  handleMessageRef.current = handleMessage;
});

useEffect(() => {
  const socket = io('wss://example.com');
  socket.on('message', (msg) => handleMessageRef.current(msg));
  return () => socket.disconnect();
}, []);

The Disable Comment โ€” Used Consciously, Not Lazily

eslint-disable isn't always wrong. It's wrong when used to silence a warning you haven't understood. Used deliberately, it's fine:

useEffect(() => {
  // Intentional: log only the value at mount, not on every change
  console.log('Initial count:', count);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Always add a comment explaining why. Future you, reviewing this at 11pm during an incident, will appreciate it. Never disable the rule just to make the yellow squiggle disappear.

Verification Steps

  • Check ESLint output: Save the file. The warning should vanish from your editor and from npx eslint src/.
  • Test the dynamic case: If the effect depends on a prop like userId, actually change that prop โ€” navigate to another user, switch a filter โ€” and confirm the effect re-runs with fresh data.
  • Watch for infinite loops: Added a function to the dependency array? Open the Network tab and check that requests aren't firing continuously. If they are, apply Fix 2 or Fix 3.
  • Run the test suite: Effects that now re-run on prop changes sometimes expose tests that were quietly passing due to stale closures. Those tests were testing the wrong thing โ€” fix them.

Quick Reference

  • Missing primitive (string, number, boolean) โ†’ add it to the array
  • Missing object or array โ†’ verify it's stable (from useState, useRef, or useMemo); memoize if not
  • Missing function from component body โ†’ move inside the effect, or wrap in useCallback
  • Missing function from props โ†’ wrap in useCallback at the call site, or move inside the effect
  • Genuinely mount-only effect โ†’ empty array is correct; add an explanation comment if you disable the lint rule

Related Error Notes