This ESLint warning occurs when the exhaustive-deps rule is not properly configured or when dependencies used inside React hooks are not included in the dependency array, potentially causing stale closures and subtle bugs.
The "react-hooks/exhaustive-deps" warning appears when ESLint's React Hooks plugin detects that you're using values inside hooks like useEffect, useMemo, or useCallback without including them in the dependency array. This rule is part of eslint-plugin-react-hooks, which is maintained by the React team to enforce the Rules of Hooks. When a value referenced inside these hooks isn't included in the dependency array, React won't re-run the effect or recalculate the value when that dependency changes, leading to stale closures. The component will continue using outdated values from previous renders, causing bugs that are often difficult to debug. The error message specifically about "recommended linter config not found" indicates that your ESLint configuration is either missing the React Hooks plugin entirely, or it's not properly configured to check for exhaustive dependencies in your effects and custom hooks.
First, check if the plugin is installed in your project:
npm list eslint-plugin-react-hooksIf not installed, add it to your project:
npm install --save-dev eslint-plugin-react-hooksOr with yarn:
yarn add --dev eslint-plugin-react-hooksFor modern ESLint (9.0.0+) using flat config format, edit eslint.config.js:
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
plugins: {
'react-hooks': reactHooks
},
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
}
];For legacy ESLint configuration (.eslintrc.json):
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}Or in .eslintrc.js:
module.exports = {
plugins: ['react-hooks'],
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
};Once the rule is configured, ESLint will warn about missing dependencies. Add all referenced values to the dependency array:
Before:
function MyComponent({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setData);
}, []); // Missing dependency: userId
}After:
function MyComponent({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setData);
}, [userId]); // Dependency added
}If you're passing functions as dependencies, wrap them in useCallback to prevent infinite re-renders:
Before:
function MyComponent() {
const handleClick = () => {
console.log('clicked');
};
useEffect(() => {
handleClick();
}, [handleClick]); // handleClick recreated every render
}After:
import { useCallback } from 'react';
function MyComponent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
useEffect(() => {
handleClick();
}, [handleClick]); // Stable reference
}Alternatively, move the function inside the effect if it's only used there:
function MyComponent() {
useEffect(() => {
const handleClick = () => {
console.log('clicked');
};
handleClick();
}, []);
}If you have custom hooks that should follow the same dependency rules, configure them using the additionalHooks option:
{
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "(useMyCustomEffect|useMyDataFetcher)"
}
]
}
}The value is a regex pattern matching the names of your custom hooks. For ESLint 6.1.1+, you can also use additionalEffectHooks in settings:
{
"settings": {
"react": {
"additionalEffectHooks": ["useMyCustomEffect"]
}
}
}After updating your ESLint configuration, restart your development tools:
VS Code:
- Open Command Palette (Cmd/Ctrl + Shift + P)
- Run "ESLint: Restart ESLint Server"
- Or reload the window: "Developer: Reload Window"
WebStorm/IntelliJ:
- File → Invalidate Caches / Restart
- Or simply restart the IDE
Command line:
npx eslint . --ext .js,.jsx,.ts,.tsxThis ensures your editor picks up the new ESLint configuration.
Understanding Stale Closures
When dependencies are missing, effects capture values from the render when they were created. If those values change in later renders but the effect doesn't re-run, you get a "stale closure" - your effect is still using old values.
When to Disable the Rule
There are legitimate cases to disable the rule using // eslint-disable-next-line react-hooks/exhaustive-deps:
1. Intentionally running only once: When you truly want an effect to run only on mount, despite using external values
2. Using refs: When accessing ref.current values that intentionally don't trigger re-renders
3. Event handlers: When functions are only used for their latest behavior, not their closure
However, these cases are rare. Most of the time, the linter is correct and you should add the dependency.
Memoization Patterns
For objects and arrays, use useMemo to maintain stable references:
const options = useMemo(() => ({
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}), []);
useEffect(() => {
fetch('/api', options);
}, [options]);Create React App Note
Projects created with Create React App include this plugin by default with the recommended configuration. If you ejected or are using a custom webpack setup, you'll need to configure it manually.
React 18+ and Concurrent Features
With React 18's concurrent rendering, stale closures can cause even more subtle issues. The exhaustive-deps rule becomes even more critical as effects may be interrupted and restarted, making proper dependency tracking essential for correctness.
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