This ESLint warning occurs when a variable or function used inside useCallback is not listed in its dependency array. The react-hooks/exhaustive-deps rule enforces complete dependency tracking to prevent stale closures and ensure callbacks update when their dependencies change.
This warning is triggered by the ESLint plugin react-hooks/exhaustive-deps rule when it detects that a callback function passed to useCallback references variables, props, or state that are not included in the dependency array. The rule exists to prevent common bugs where callbacks capture stale values from previous renders. When you use useCallback, React memoizes the callback function and only recreates it when one of the dependencies in the array changes. If you reference a variable inside the callback but don't include it in the dependency array, the callback will continue using the old value from when it was first created, even if that value has changed in subsequent renders. This creates a "stale closure" problem that can lead to incorrect behavior. The exhaustive-deps rule analyzes your callback body and compares all referenced values against your dependency array. It warns you about any missing dependencies so you can decide whether to add them, restructure your code, or (rarely) explicitly acknowledge that you want the stale behavior.
The most straightforward solution is to include all referenced variables in the dependency array. Review the ESLint warning to identify which dependency is missing, then add it:
// Before: Missing dependency warning
const handleClick = useCallback(() => {
console.log(userId); // userId not in dependencies
}, []);
// After: Include the missing dependency
const handleClick = useCallback(() => {
console.log(userId);
}, [userId]); // Now callback updates when userId changesThis ensures the callback is recreated whenever the dependency changes, preventing stale closures.
If you're calling another function inside useCallback, and that function references props or state, consider moving it inside the callback or wrapping it in its own useCallback:
// Before: Helper function causes dependency issues
function processData(value: string) {
return value + userId; // References userId from component scope
}
const handleSubmit = useCallback(() => {
const result = processData(input);
// ...
}, [input]); // Missing userId dependency
// After: Move function inside callback
const handleSubmit = useCallback(() => {
function processData(value: string) {
return value + userId; // Now clearly requires userId
}
const result = processData(input);
// ...
}, [input, userId]); // All dependencies explicitIf your callback updates state based on the current state value, use the functional update form instead of referencing state directly. This eliminates the need to include state in dependencies:
// Before: State in dependencies
const increment = useCallback(() => {
setCount(count + 1); // References current count
}, [count]); // Callback recreated every time count changes
// After: Functional update removes dependency
const increment = useCallback(() => {
setCount(prev => prev + 1); // No reference to count
}, []); // Stable callback, never recreatedThis pattern is especially useful for event handlers that should remain stable across renders.
Objects and arrays created inline get new references every render, causing useCallback to recreate the function even if the content hasn't changed. Memoize them first:
// Before: Object recreated every render
const config = { apiKey, timeout: 5000 };
const fetchData = useCallback(() => {
api.fetch(config); // config is a new object every time
}, [config]); // Callback recreates on every render
// After: Memoize the object
const config = useMemo(() => ({
apiKey,
timeout: 5000
}), [apiKey]);
const fetchData = useCallback(() => {
api.fetch(config);
}, [config]); // Now only recreates when apiKey changesIf a value never changes, move it outside the component so it doesn't need to be a dependency:
// Before: Constant in component scope
function MyComponent() {
const API_ENDPOINT = 'https://api.example.com';
const fetchData = useCallback(() => {
fetch(API_ENDPOINT); // ESLint wants this as dependency
}, []); // Warning about missing API_ENDPOINT
}
// After: Move constant outside
const API_ENDPOINT = 'https://api.example.com';
function MyComponent() {
const fetchData = useCallback(() => {
fetch(API_ENDPOINT); // No longer a dependency
}, []); // No warning
}If you need to reference a value inside the callback but don't want changes to that value to recreate the callback, store it in a ref:
// Before: Callback recreates on every analytics change
const handleClick = useCallback(() => {
analytics.track('click', { userId });
}, [userId, analytics]); // Analytics object changes frequently
// After: Store analytics in ref
const analyticsRef = useRef(analytics);
analyticsRef.current = analytics; // Keep ref updated
const handleClick = useCallback(() => {
analyticsRef.current.track('click', { userId });
}, [userId]); // Callback only recreates when userId changesUse this pattern sparingly and only when you're certain the callback doesn't need to update when the ref'd value changes.
If you've carefully considered your use case and determined the dependency should intentionally be omitted (rare), you can disable the warning with a comment:
const handleMount = useCallback(() => {
// This should only run once on mount, not when userId changes
initializeComponent(userId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Intentionally empty despite using userIdWarning: Only use this if you fully understand the implications. Missing dependencies usually indicate real bugs. Document why the dependency is intentionally omitted and consider if useEffect might be more appropriate.
Understanding the exhaustive-deps rule: The react-hooks/exhaustive-deps ESLint rule is part of the eslint-plugin-react-hooks package. It analyzes your hook code statically and warns about any values used inside hooks that aren't in the dependency array. This rule works with useCallback, useMemo, useEffect, and custom hooks that follow the dependencies pattern.
When callbacks recreate too often: If adding all dependencies causes your callback to recreate on every render, it defeats the purpose of memoization. This usually indicates a code structure issue. Consider: (1) whether you actually need useCallback at all - it's only beneficial when passing callbacks to memoized child components or using them in other hook dependencies, (2) whether upstream values should be memoized first with useMemo, or (3) whether your component is doing too much and should be split.
Custom hooks and dependencies: If you're writing custom hooks that accept dependency arrays, you can configure the exhaustive-deps rule to validate them using the additionalHooks option in your ESLint config. This ensures your custom hooks receive the same lint protection as built-in hooks.
React Compiler future: The experimental React Compiler (formerly React Forget) aims to automatically memoize components and hooks without requiring manual useCallback, useMemo, or dependency arrays. When it's stable, many of these warnings may become obsolete. Until then, follow the exhaustive-deps rule.
Performance considerations: Don't use useCallback prematurely for performance. Wrapping every function in useCallback actually adds overhead unless those functions are passed to optimized components wrapped in React.memo or used as dependencies in other hooks. Profile first, optimize second.
Debugging stale closures: If you suspect a stale closure bug, add a console.log inside your callback to see which values it's capturing. Compare those values to what you expect. If they're outdated, you're missing a dependency. The React DevTools Profiler can also help identify components rendering unnecessarily due to changing callbacks.
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
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