Back to blog

How to Fix React useEffect Missing Dependency Lint Warning

When developing React components, one of the most persistent linting warnings issued by ESLint (specifically the react-hooks/exhaustive-deps rule) is:

React Hook useEffect has a missing dependency: 'xxx'. 
Either include it or remove the dependency array.

Many developers bypass this warning by adding eslint-disable-next-line to the dependency array.

Do not do this. This warning exists to prevent one of React's most difficult bugs to debug: Stale Closures.

In this guide, we will analyze why React requires complete dependency arrays, examine why stale closures happen, and walk through four refactoring patterns to resolve the linting warning safely.

Why Does React Demand Complete Dependency Arrays?

A useEffect hook captures the values of variables (props, state, or functions) from the current render cycle.

If you read a variable inside the effect but omit it from the dependency array, the effect will not run again when that variable changes. It continues to reference the value from the initial render—resulting in a stale closure:

// AVOID: Stale closure bug
const [count, setCount] = useState(0);

useEffect(() => {
  const timer = setInterval(() => {
    // count is captured as 0 from the first render.
    // The timer will repeatedly set count to 0 + 1, locking the value at 1.
    console.log("Count inside effect:", count); 
    setCount(count + 1);
  }, 1000);

  return () => clearInterval(timer);
}, []); // Warning: 'count' is missing from the dependency list

To prevent this, React demands that every variable accessed inside the effect function must be listed in the dependency array.

Solution 1: Add the Dependency and Use Functional Updates

If you only need to read a state variable to calculate the next state, you can eliminate the dependency entirely by using a Functional State Update:

// Good: State updater removes the need to read 'count' directly
const [count, setCount] = useState(0);

useEffect(() => {
  const timer = setInterval(() => {
    // React passes the latest state value into the updater callback automatically
    setCount((prevCount) => prevCount + 1);
  }, 1000);

  return () => clearInterval(timer);
}, []); // Safe: 'count' is no longer read in the effect, no dependency needed

By passing a callback function (prevCount) => prevCount + 1 to setCount, you no longer read the count variable inside the effect.

Solution 2: Stabilize Functions with useCallback

If your effect calls a function declared inside the component body, adding that function to the dependency array can cause an infinite loop.

Because JavaScript objects and functions compare by reference, the function is recreated on every single render. This triggers the effect, which updates state, triggering a render, recreating the function, and running the effect again.

// AVOID: Function recreation triggers infinite loops
const fetchUser = async () => {
  const res = await fetch(`/api/users/${userId}`);
  setData(await res.json());
};

useEffect(() => {
  fetchUser();
}, [fetchUser]); // Runs on EVERY render because fetchUser reference changes
  • The Fix: Wrap the function in the useCallback hook to memoize its reference:
// Good: useCallback keeps the reference stable unless userId changes
const fetchUser = useCallback(async () => {
  const res = await fetch(`/api/users/${userId}`);
  setData(await res.json());
}, [userId]);

useEffect(() => {
  fetchUser();
}, [fetchUser]); // Safe: fetchUser only changes when userId changes

Solution 3: Move Functions Outside the Component

If a function does not read any state variables or props from the component, you can move its definition outside the component body.

// Good: Declared outside the component, so its reference never changes
const formatUrl = (id: string) => `/api/users/${id}`;

export function UserProfile({ userId }: { userId: string }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const url = formatUrl(userId);
    fetch(url).then(res => res.json()).then(setData);
  }, [userId]); // Only needs 'userId' as a dependency
}

Since formatUrl is outside the component, it is outside React's rendering loop, removing it from the dependency requirements.

Solution 4: Use a Ref to Track Dynamic Values (Last Resort)

If you must read a highly dynamic value inside an effect but absolutely do not want the effect to trigger again when that value changes, store the value in a mutable useRef object.

Reading .current from a ref does not trigger renders and is not tracked by the React dependency analyzer:

const [data, setData] = useState(null);
const latestDataRef = useRef(data);

// Keep the ref updated on every render
useEffect(() => {
  latestDataRef.current = data;
});

useEffect(() => {
  // Read from the ref safely without adding 'data' to the dependencies
  console.log("Latest data inside mounted effect:", latestDataRef.current);
}, []); // Runs once on mount

Conclusion

The useEffect missing dependency warning protects your React applications from stale closures. To resolve this lint warning, leverage functional state updates to remove direct state variables reads, wrap component-level functions in useCallback to prevent infinite rendering cycles, relocate static helpers outside component closures, or store dynamic trackers in useRef to read mutable values safely.