๐ŸŒ Detecting your locationโ€ฆ

How to Fix ‘React Hook useEffect has a missing dependency’ Warning

โฑ๏ธ6 min read  ยท  1,187 words

React Hook useEffect has a missing dependency: 'x'. Either include it or remove the dependency array. Most developers silence this with an eslint-disable comment. That works until it causes a stale-data bug that takes a day to find. The warning is usually correct, and there is a right fix for each situation.

Why the Warning Exists

The dependency array tells React when to re-run an effect. If the effect reads a value that is not listed, the effect keeps the value from the render in which it last ran โ€” a stale closure. The code looks correct and behaves incorrectly.

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

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, []);   // โš ๏ธ missing dependency: 'userId'

  return <div>{user?.name}</div>;
}

With an empty array the fetch runs once. Navigate from user 1 to user 2 and the component keeps showing user 1 forever. The warning predicted exactly this.

useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(r => r.json())
    .then(setUser);
}, [userId]);   // โœ… refetches whenever userId changes

Fix 1: Just Add the Dependency

In the majority of cases the warning is right and adding the dependency is the whole fix. Do this first and only investigate further if it causes a loop.

Fix 2: The Effect Loops After Adding It

Adding an object, array, or function to the dependency array often causes an infinite loop, because those are recreated on every render and compared by reference.

function Search({ filters }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    search(filters).then(setResults);
  }, [filters]);   // new object identity every render -> infinite loop
}

Depend on the primitive values instead of the object.

useEffect(() => {
  search({ query, category }).then(setResults);
}, [query, category]);   // strings compare by value โ€” stable

When the object genuinely comes from a parent, memoise it there.

// Parent
const filters = useMemo(
  () => ({ query, category }),
  [query, category]
);
return <Search filters={filters} />;

Fix 3: Function Dependencies โ€” useCallback

A function declared in the component body is a new value every render, so listing it re-runs the effect endlessly.

function Dashboard({ userId }) {
  // Recreated every render.
  const loadData = async () => {
    const res = await fetch(`/api/data/${userId}`);
    return res.json();
  };

  useEffect(() => {
    loadData().then(setData);
  }, [loadData]);   // loops
}

Two correct fixes. If the function is used only by the effect, move it inside โ€” the cleanest option, because the dependency disappears entirely.

useEffect(() => {
  async function loadData() {
    const res = await fetch(`/api/data/${userId}`);
    setData(await res.json());
  }
  loadData();
}, [userId]);   // โœ… only the primitive is a dependency

If the function is shared with other parts of the component, wrap it in useCallback.

const loadData = useCallback(async () => {
  const res = await fetch(`/api/data/${userId}`);
  return res.json();
}, [userId]);

useEffect(() => {
  loadData().then(setData);
}, [loadData]);   // โœ… identity only changes when userId changes

Fix 4: You Need the Latest Value Without Re-Running

Sometimes an effect legitimately should run once, but the callback inside needs current values. A ref holds a mutable value that does not participate in dependencies.

function Chat({ onMessage }) {
  const onMessageRef = useRef(onMessage);

  // Keep the ref current on every render.
  useEffect(() => {
    onMessageRef.current = onMessage;
  });

  useEffect(() => {
    const socket = new WebSocket('wss://example.com');
    socket.onmessage = (e) => onMessageRef.current(e.data);
    return () => socket.close();
  }, []);   // โœ… connects once, always calls the latest handler
}

This is the correct pattern for subscriptions, timers, and event listeners where reconnecting on every prop change would be wrong.

Fix 5: Updating State From Its Own Previous Value

Reading state to compute the next state makes that state a dependency, which usually causes a loop. The updater form removes the dependency.

// Loops: count changes -> effect re-runs -> count changes...
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, [count]);

// โœ… No dependency on count at all.
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);

When Disabling the Rule Is Actually Correct

There is one legitimate case: a true mount-only effect where you deliberately want the initial value and nothing else. Analytics on first render is the standard example.

useEffect(() => {
  analytics.track('page_view', { page: pageName });
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);   // Intentional: fire once with the initial pageName.

If you disable the rule, write a comment explaining why the omission is deliberate. A bare disable comment is indistinguishable from someone who did not understand the warning, including to yourself in six months.

Do You Even Need an Effect?

A large share of exhaustive-deps warnings come from effects that should not exist. Two common cases:

Derived state. Computing a value from props or state does not need an effect.

// Unnecessary effect, extra render, and a dependency warning.
const [fullName, setFullName] = useState('');
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);

// โœ… Just calculate it during render.
const fullName = `${first} ${last}`;

Responding to an event. Logic that belongs in an event handler often ends up in an effect watching state that the handler set.

// โœ… Do the work where the event happens.
function handleSubmit() {
  submitForm(data);
  analytics.track('form_submitted');
}

Effects are for synchronising with systems outside React โ€” network, subscriptions, the DOM, timers. If nothing external is involved, an effect is probably the wrong tool.

Decision Guide

Situation Fix
Warning names a primitive Add it to the array
Object or array causes a loop Depend on its primitive fields, or useMemo at the source
Function causes a loop Move it inside the effect, or useCallback
Need the latest value, must not re-run Store it in a ref
State derives from previous state Use the updater function form
Value is computed from props or state Delete the effect and compute during render
Genuinely mount-only Disable the rule with a written justification

Frequently Asked Questions

Q: Why does my effect run twice in development?
A: Strict Mode intentionally mounts, unmounts, and remounts components to surface missing cleanup functions. It does not happen in production. If double-running breaks something, your effect is missing a cleanup.

Q: Can I just turn off the exhaustive-deps rule?
A: You can, and you will ship stale-closure bugs that are extremely hard to trace. The rule catches real defects; keep it on and fix the causes.

Q: Does useCallback everywhere hurt performance?
A: Slightly โ€” it costs memory and comparison work. Use it where identity stability actually matters, such as dependency arrays and memoised children, not reflexively on every function.

Q: What about the React Compiler?
A: It automates much of the memoisation that useCallback and useMemo do by hand, which removes a category of these warnings. It does not remove the need to understand which values your effect actually depends on.

Q: My effect needs a value but should not re-run when it changes. Is a ref a hack?
A: No, it is the documented pattern for that situation. Keeping the ref updated in a separate effect and reading .current inside the long-lived effect is idiomatic.

Conclusion

The exhaustive-deps warning is a bug detector, not a nuisance. Add the dependency first; if that loops, fix the identity problem with primitives, useMemo, or useCallback; use a ref when you need current values without re-running; use the state updater form to break self-referential loops; and delete the effect entirely when the value is merely derived. Reserve eslint-disable for genuine mount-only effects, and always leave a comment saying why.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work โ€” which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *

๐ŸŒ Read in:๐Ÿ‡ฌ๐Ÿ‡ง English๐Ÿ‡ฉ๐Ÿ‡ช Deutsch๐Ÿ‡ง๐Ÿ‡ท Portuguรชs๐Ÿ‡ธ๐Ÿ‡ฆ ุงู„ุนุฑุจูŠุฉ๐Ÿ‡ฎ๐Ÿ‡ณ เคนเคฟเคจเฅเคฆเฅ€๐Ÿ‡ง๐Ÿ‡ฉ เฆฌเฆพเฆ‚เฆฒเฆพ