ESLint warning from the exhaustive-deps rule indicating that a variable, prop, or state value referenced inside useCallback is not included in its dependency array. This can lead to stale closures and bugs where the callback uses outdated values.
This ESLint warning comes from the `react-hooks/exhaustive-deps` rule, which is part of the official `eslint-plugin-react-hooks` package maintained by the React team. The rule analyzes your `useCallback` hook and verifies that every value referenced inside the callback function is also listed in the dependency array. When you use `useCallback`, React memoizes the function and only creates a new function instance when one of the dependencies changes. If you reference a variable, prop, or state value inside the callback but don't include it in the dependency array, the callback will continue using the value from when it was first created—not the current value. This creates a "stale closure" where your callback operates on outdated data. The exhaustive-deps rule exists to prevent subtle bugs that are difficult to debug. While it's tempting to disable the warning, doing so often leads to race conditions, incorrect behavior, and unpredictable component state.
The most straightforward fix is to include the missing dependency in the array:
// Before (causes warning)
const handleClick = useCallback(() => {
console.log(count); // count is used but not in deps
}, []);
// After (warning fixed)
const handleClick = useCallback(() => {
console.log(count);
}, [count]); // count added to dependency arrayThis ensures the callback always has access to the current value of count. The callback will be recreated whenever count changes.
If you're referencing another function inside useCallback, wrap that function in its own useCallback to maintain a stable reference:
// Before (causes warning)
const fetchData = () => {
api.getData(userId);
};
const handleRefresh = useCallback(() => {
fetchData(); // fetchData recreated every render
}, []);
// After (warning fixed)
const fetchData = useCallback(() => {
api.getData(userId);
}, [userId]);
const handleRefresh = useCallback(() => {
fetchData(); // stable reference
}, [fetchData]);This creates a chain of properly memoized functions where each declares its dependencies correctly.
If your callback depends on an object or array, memoize it with useMemo to prevent unnecessary recreations:
// Before (causes warning)
const config = { userId, apiKey };
const handleSubmit = useCallback(() => {
api.send(config); // config is a new object every render
}, []);
// After (warning fixed)
const config = useMemo(() => ({
userId,
apiKey
}), [userId, apiKey]);
const handleSubmit = useCallback(() => {
api.send(config);
}, [config]);Without useMemo, the object is recreated on every render, causing useCallback to see it as a "changed" dependency.
When your callback only needs to update state based on the previous value, use the functional update form to avoid depending on the state variable:
// Before (requires count in deps)
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
// After (no dependency needed)
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);This pattern is especially useful for callbacks that should have a stable reference but need to update state.
If you need to access the latest value of something without causing the callback to be recreated, store it in a ref:
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // Always keep ref updated
}, [count]);
const handleClick = useCallback(() => {
console.log(countRef.current); // Access latest value
}, []); // Empty deps - callback never recreatedThis is useful when you want a stable callback reference but need access to current values. Use this pattern sparingly and only when you understand the tradeoffs.
In rare cases where you intentionally want to capture the initial value and ignore updates, disable the rule with a comment explaining why:
const handleMount = useCallback(() => {
// We intentionally only want the initial userId value
// eslint-disable-next-line react-hooks/exhaustive-deps
console.log(userId);
}, []);Always add a comment explaining why the rule is disabled. This helps future maintainers understand the intentional deviation. In most cases, one of the solutions above is preferable.
Why the exhaustive-deps rule exists
The React team created this rule because stale closures are one of the most common sources of bugs in React applications. When a callback "closes over" a variable but doesn't declare it as a dependency, the callback continues using the value from when it was created, not the current value. This leads to subtle bugs that are hard to reproduce and debug.
Performance considerations
Some developers worry that adding dependencies will cause too many re-renders. However, if your callback needs the current value of a dependency, it *should* be recreated when that value changes—that's correct behavior. If you're concerned about performance:
1. Measure first—premature optimization causes more bugs than it solves
2. Use React DevTools Profiler to identify actual performance bottlenecks
3. Consider if you actually need useCallback at all—it's only beneficial when passing callbacks to memoized child components
React 19 and the React Compiler
React 19 introduces an experimental compiler that can automatically handle memoization, potentially reducing the need for manual useCallback usage. However, the exhaustive-deps rule will remain important for ensuring correct dependencies even when the compiler handles the memoization.
ESLint autofix
The exhaustive-deps rule includes an autofix feature. In VS Code or other editors with ESLint integration, you can often press Ctrl+. (or Cmd+.) on the warning and select "Fix this exhaustive-deps warning" to automatically add the missing dependencies. Always review the autofix to ensure it makes sense for your use case.
Dependency array complexity
If you find yourself with a useCallback that has 5+ dependencies, consider whether:
- The function is doing too much and should be split up
- Some dependencies could be combined into a single object
- The callback actually needs to be memoized at all
Complex dependency arrays are often a code smell indicating the component has too many responsibilities.
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite
Vite HMR connection failed, make sure your config is correct
How to fix "Vite HMR connection failed" in React