Skip to content

How to Fix ESLint useEffect Dependency Warnings in React

Problem

I was working on a React component when I got this ESLint warning:

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

My code looked like this:

UserList.jsx
function UserList({ userId }) {
const [users, setUsers] = useState([]);
const fetchData = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUsers(data);
};
useEffect(() => {
fetchData();
}, []); // ESLint warning here!
return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

I kept getting the warning and didn’t know what to do. I was tempted to just disable the rule with an eslint-disable comment.

Why This Warning Matters

The ESLint plugin eslint-plugin-react-hooks has a rule called react-hooks/exhaustive-deps that warns you when your useEffect dependencies are incomplete. This isn’t just a linting annoyance—it catches real bugs.

When you have a dependency array like [ ], you’re telling React: “This effect doesn’t depend on any props or state, so only run it once when the component mounts.” But if your effect uses variables like fetchData or userId, and those values change, your effect won’t re-run, leading to stale data or bugs.

From the React docs:

If you specify a list of dependencies, React will only re-run the effect if at least one of the dependencies in the list has changed.

Solution 1: Add Missing Dependencies

The simplest fix is to add the missing dependencies to the array:

UserList.jsx
function UserList({ userId }) {
const [users, setUsers] = useState([]);
const fetchData = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUsers(data);
};
useEffect(() => {
fetchData();
}, [fetchData]); // Added fetchData as dependency
return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

But wait—this causes an infinite loop! Every render creates a new fetchData function, so the effect runs again, causing a re-render, creating a new fetchData, and so on.

Solution 2: Use useCallback to Stabilize Functions

To fix the infinite loop, I wrapped fetchData with useCallback:

UserList.jsx
function UserList({ userId }) {
const [users, setUsers] = useState([]);
const fetchData = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUsers(data);
}, [userId]); // fetchData only changes when userId changes
useEffect(() => {
fetchData();
}, [fetchData]); // Now this works correctly
return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

Now fetchData only gets recreated when userId changes. This is the pattern the ESLint rule is guiding you toward.

Solution 3: Move the Function Inside the Effect

Another clean approach is to move the function definition inside the effect:

UserList.jsx
function UserList({ userId }) {
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUsers(data);
};
fetchData();
}, [userId]); // Only userId is the dependency now
return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

This approach avoids the need for useCallback and makes the dependencies clearer.

Solution 4: Use the Functional Update Form of setState

If your effect only calls setState and doesn’t need to read the current state, use the functional update form:

Counter.jsx
function Counter({ increment }) {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + increment); // Functional update doesn't need 'count' in deps
}, 1000);
return () => clearInterval(interval);
}, [increment]); // Only 'increment' is needed
return <div>Count: {count}</div>;
}

Solution 5: Use useRef for Mutable Values

If you need a mutable value that doesn’t trigger re-renders and doesn’t need to be in the dependency array, use useRef:

Timer.jsx
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []); // Empty array is fine here
return <div>Seconds: {seconds}</div>;
}

Common Mistakes to Avoid

Never Suppress the Warning

BAD - Don't do this!
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

This hides the warning but not the bug. The ESLint rule is smarter than you think—it catches issues that will cause problems in production.

Don’t Lie About Dependencies

BAD - Don't do this!
useEffect(() => {
fetchData(); // Uses userId internally
}, []); // Lying: saying it doesn't depend on anything

If userId changes, your effect won’t re-run, leading to stale data.

Understand What Goes in Dependencies

The dependency array should include all values from the component scope that are used inside the effect:

  • Props (like userId)
  • State variables (like users)
  • Functions defined in the component body (unless moved inside the effect)

The Key Insight

As one Reddit commenter put it: “Just add the ESLint rule around hooks and it will warn you.” The exhaustive-deps rule isn’t nagging you—it’s catching bugs. Every time I’ve suppressed this warning, I’ve regretted it later.

The warning is essentially asking: “Are you sure this effect should only run when these specific values change?” If you’re using something in the effect that’s not in the dependencies, you’re probably introducing a bug.

Summary

  1. Add all variables used in the effect to the dependency array
  2. Use useCallback for functions to prevent unnecessary recreations
  3. Move functions inside the effect if they’re only used there
  4. Use functional state updates when possible
  5. Never suppress the warning—fix the underlying issue instead

The ESLint exhaustive-deps rule is one of the most valuable tools in React development. Trust it, understand it, and your code will have fewer bugs.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments