React Hook useEffect has a missing dependency: the fix
The three warnings this rule prints for one fetch function, which remedy each one wants, and a measured run showing what silencing it costs when a prop changes.
TL;DR#
The warning comes from the react-hooks/exhaustive-deps ESLint rule, not from React at runtime. Your effect calls a function declared in the component body, and that function reads a prop or state value, so the effect depends on it without listing it. The fix is to move the function inside the effect and list the values it reads, or, when the function is also used elsewhere, wrap it in useCallback and list the callback. Adding the bare function to the array swaps this warning for a second one and, if the effect sets state, an endless fetch loop.
This is the question asked on Stack Overflow (55840294), where a component fetched data in a useEffect with an empty array and got:
14:6 warning React Hook useEffect has a missing dependency: 'fetchBusinesses'. Either include it or remove the dependency array react-hooks/exhaustive-depsThat line, and every other warning and count on this page, was reproduced on 30 October 2026 in a node:24-alpine container with ESLint 10.14.0 (flat config), eslint-plugin-react-hooks 7.1.1 and React 19.3.1, all installed from the npm registry that day.
What the rule is checking#
The component that triggers it looks like this. The function reads the city prop, and the effect calls the function:
import { useEffect, useState } from 'react';
export default function BusinessesList({ city }) {
const [businesses, setBusinesses] = useState([]);
const fetchBusinesses = () => {
return fetch(`/api/businesses?city=${city}`)
.then((res) => res.json())
.then((rcvd) => setBusinesses(rcvd));
};
useEffect(() => {
fetchBusinesses();
}, []);
return <ul>{businesses.map((b) => <li key={b.id}>{b.name}</li>)}</ul>;
}The exhaustive-deps rule page states the reason: when a value used inside a hook is missing from its dependency array, React does not re-run the effect when that value changes, and the effect keeps reading stale values. Here the empty array tells React that the effect never needs to re-run. That is false: fetchBusinesses closes over city, so the effect really does depend on city, through the function.
The rule only reports functions that can change. In the reproduction, the same component without the city prop (the function reads only fetch and the setBusinesses setter, which React keeps stable) produced no warning at all. The rule's source has a dedicated check for "functions that don't reference anything dynamic". So with the current plugin, the warning means the function captures something that is not stable: a prop, a piece of state, or another function or value recreated on each render. The question does not show which value its function captured.
The fix: move the function or give it a stable identity#
1. Move the function inside the effect#
If only the effect calls the function, define it inside the effect. Then the rule can see exactly what the effect reads, and you list that instead:
import { useEffect, useState } from 'react';
export default function MovedInside({ city }) {
const [businesses, setBusinesses] = useState([]);
useEffect(() => {
let ignore = false;
const fetchBusinesses = () => {
return fetch(`/api/businesses?city=${city}`)
.then((res) => res.json())
.then((rcvd) => {
if (!ignore) setBusinesses(rcvd);
});
};
fetchBusinesses();
return () => {
ignore = true;
};
}, [city]);
return <ul>{businesses.map((b) => <li key={b.id}>{b.name}</li>)}</ul>;
}This is the accepted answer's first suggestion, and the one React's own guide to removing Effect dependencies gives for functions. The ignore flag is the pattern from the useEffect reference for fetching: React runs the cleanup before the effect re-runs for a new city and when the component unmounts, so a late response for the previous city is ignored instead of overwriting the result for the new one. AbortController can also cancel the network request itself; that approach is covered in Can't perform a state update on an unmounted component.
ESLint reports nothing for this file. With [city] the effect re-runs when the city changes, which is what the original [] silently prevented.
2. Wrap it in useCallback when something else calls it too#
If a button or another handler also calls the function, keep it in the component body and give it a stable identity with useCallback. The callback lists what it reads, and the effect lists the callback:
import { useCallback, useEffect, useState } from 'react';
export default function WithCallback({ city }) {
const [businesses, setBusinesses] = useState([]);
const fetchBusinesses = useCallback(() => {
return fetch(`/api/businesses?city=${city}`)
.then((res) => res.json())
.then((rcvd) => setBusinesses(rcvd));
}, [city]);
useEffect(() => {
fetchBusinesses();
}, [fetchBusinesses]);
return (
<>
<button onClick={fetchBusinesses}>Refresh</button>
<ul>{businesses.map((b) => <li key={b.id}>{b.name}</li>)}</ul>
</>
);
}This version fixes the warning and refetches when the city changes, but it has no ignore flag: if the response for the previous city arrives after the one for the new city, it still overwrites the list. When responses can arrive out of order, keep the request inside the effect as in option 1, and let the button trigger a refetch through state instead.
The useCallback array is checked by the same rule. Writing useCallback(..., []) around a function that reads city only moves the warning:
10:6 warning React Hook useCallback has a missing dependency: 'city'. Either include it or remove the dependency array react-hooks/exhaustive-deps3. What not to do: add the bare function to the array#
The warning text says "Either include it", so the obvious edit is }, [fetchBusinesses]); with the function still declared as a plain arrow function. The rule then reports a different problem at the function's declaration:
6:9 warning The 'fetchBusinesses' function makes the dependencies of useEffect Hook (at line 14) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'fetchBusinesses' in its own useCallback() Hook react-hooks/exhaustive-depsThat second warning is right, and it is not cosmetic. Every render creates a new fetchBusinesses, so the dependency changes on every render, the effect runs again, setBusinesses stores a new array, and the component renders again. The reproduction mounted each version in jsdom with a stubbed fetch and counted calls, first after 300 ms, then (cumulatively) after changing the city prop from Lyon to Porto. There was no StrictMode wrapper:
NaiveDep (no useCallback): fetches after 300 ms = 971, after city change = 4535
WithCallback ([city]): fetches after 300 ms = 1, after city change = 2
MovedInside ([city]): fetches after 300 ms = 1, after city change = 2The naive version made hundreds of requests in 300 ms; the exact figure depends on timing, and the count was still climbing when the component was unmounted. Both fixes fetch once on mount and once when the city changes.
The eslint-disable comment and what it breaks#
Both the accepted answer and the second answer on the question offer a disable comment as a way out: // eslint-disable-next-line react-hooks/exhaustive-deps on the line above the array in the first, // eslint-disable-line react-hooks/exhaustive-deps at the end of the array line in the second. It removes the warning and keeps the bug the warning was pointing at. The same run measured it:
Disabled ([] + eslint-disable): fetches after 300 ms = 1, after city change = 1The component fetched for Lyon, then the prop changed to Porto and nothing happened: the list on screen still showed Lyon's businesses. The comment also hides every later warning in that dependency array, including one for a value someone adds next year. The React guide to removing Effect dependencies is direct about it: "We recommend treating the dependency lint error as a compilation error. If you don't suppress it, you will never see bugs like this."
The comment is only defensible when the function truly reads nothing that changes. In that case the rule usually stays silent anyway, as shown above, or the function can move outside the component.
useEffectEvent: for events, not for silencing the rule#
React 19.2 shipped useEffectEvent in a stable release. It lets an effect call logic that reads the latest props without re-running when they change. The legitimate use is an event the effect fires, such as a toast after the data arrives that should use the current theme, where a theme change must not trigger a new fetch:
import { useEffect, useEffectEvent, useState } from 'react';
import { showToast } from './toast.js';
export default function EffectEvent({ city, theme }) {
const [businesses, setBusinesses] = useState([]);
// An event fired by the Effect: reads the latest theme, never re-runs the fetch.
const onLoaded = useEffectEvent((count) => {
showToast(`${count} businesses loaded`, theme);
});
useEffect(() => {
let ignore = false;
fetch(`/api/businesses?city=${city}`)
.then((res) => res.json())
.then((rcvd) => {
if (ignore) return;
setBusinesses(rcvd);
onLoaded(rcvd.length);
});
return () => {
ignore = true;
};
}, [city]);
return <ul>{businesses.map((b) => <li key={b.id}>{b.name}</li>)}</ul>;
}The rule accepts this with [city] only. Listing the Effect Event as well is itself a warning:
24:13 warning Functions returned from `useEffectEvent` must not be included in the dependency array. Remove `onLoaded` from the list react-hooks/exhaustive-depsThe useEffectEvent reference warns against the shortcut this question invites. Wrapping fetchBusinesses in useEffectEvent so that city disappears from the array would bring back the stale-city bug from the previous section. If a value should re-run the fetch, it stays a dependency.
Flat config: where the warning comes from#
The reproduction used flat config (eslint.config.js), once with a single rule entry and once with the plugin's preset. Both printed the same four warnings:
import reactHooks from 'eslint-plugin-react-hooks';
export default [
{
files: ['src/**/*.jsx'],
languageOptions: {
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
reactHooks.configs.flat.recommended,
];The preset sets exhaustive-deps to "warn", and a warning alone does not make ESLint exit with an error by default, so the problem is easy to ship. In Next.js 16, which no longer lints during next build, this file is what decides whether the rule runs at all. The migration is covered in Next.js 16 removed next lint.
Confirm the warning is gone for the right reason#
Run npx eslint src/ and check the component's line no longer appears. Then change the prop the function reads, which here is the city, and confirm in the browser's Network tab that exactly one new request goes out. In development the first request on mount may appear twice: that is Strict Mode re-running effects on purpose, not a new loop. If that double fetch is what you see next, continue with why useEffect runs twice in React 18, which explains the cleanup that makes it harmless.
The warning is short, but it names the value React cannot see. Moving the function into the effect or giving it a stable identity with useCallback fixes the warning and makes the effect refetch when the city changes; the ignore cleanup of option 1 also keeps a late response from overwriting the new one. A disable comment fixes only the warning.
Further reading: MUI DatePicker readonly in Cypress CI: fix the variant.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix: Can't Perform a State Update on an Unmounted Component
Separate the old React warning from a persistent resource leak and from out-of-order responses while the component is still mounted.
React Hook Form: Fix Confirm Password Validation
Block mismatched submissions, display the field error, and catch a confirmation that becomes stale after the user edits the original password.
Fix: Objects Are Not Valid as a React Child
The 2015 bug reproduced today, the exact modern error text, what the production build shows instead, and the one-line fix for every cause.


