How the React Hooks ESLint plugin saved me hours debugging useEffect

Ever find yourself chasing weird bugs in React, like sometimes it seems as though your components fetch all of the data they need, and sometimes they don't?

Chances are you've probably got yourself a race condition.

That's the special insight I'm giving you after spending several days at work chasing weird behaviour in one of our React apps, that could only be reproduced once in 75 page refreshes.

In case you're wondering, here's some backstory.

Imagine your web app needs to make calls to several APIs to get the information it needs to render your component:

useEffect(() => {
// assume these API calls just write to a global state store
// and the input state is also coming from that global state store
getDataFromFirstAPI(someState);
getDataFromSecondAPI(someOtherState);
getDataFromThirdAPI(someThirdState);
}, [someState, someOtherState]);

What would happen is that either of getDataFromFirstAPI or getDataFromSecondAPI could mutate the value of someThirdState (as a feature of our particular implementation), making calls to getDataFromThirdAPI use stale data.

Sharp readers will notice the fix pretty quickly: someThirdState is missing from the dependency array, but if your component was complex and had several useEffect calls already, chances are you probably wouldn't notice it. Especially if several of those useEffect calls also have missing dependencies.

In fact, create-react-app would only tell you about it via a warning, not an error. In our case, the warning would be buried deep in a stack of noisy logs (as part of the review into this type of bug, as a team we decided to make it an error, but it's up to you and your team).

Turns out there's an eslint rule specifically for this class of bug: react-hooks/exhaustive-deps, AND it's part of a package maintained by the React team.

That's where eslint-plugin-react-hooks comes in. You already have it if you use create-react-app, but chances are you don't have it if you've decided to go with a custom webpack config.

You add it via npm/yarn:

yarn add eslint-plugin-react-hooks -D
# OR
npm i eslint-plugin-react-hooks -D

Afterwards, you can add it to your ESLint config like so:

// Your ESLint configuration
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
}
}

Now every time you hit save in your codebase with missing dependencies in your useEffects, you'll have a similar warning/error to this show up:

Compiled with warnings.
./src/App.js
Line 16:6: React Hook useEffect has a missing dependency: 'someThirdState'. Either include it or remove the dependency array react-hooks/exhaustive-deps
Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.

Since enabling the react-hooks/exhaustive-deps rule, I've already saved myself hours of head scratching and general outrage by saving myself from committing buggy code several times.

Do yourself a favour and check if you've got the rule enabled.

(Shameless plug for the useEffect book I wrote below)

Tired of infinite re-renders when using useEffect?

A few years ago when I worked at Atlassian, a useEffect bug I wrote took down part of Jira for roughly one hour.

Knowing thousands of customers can't work because of a bug you wrote is a terrible feeling. To save others from making the same mistakes, I wrote a single resource that answers all of your questions about useEffect, after teaching it here on my blog for the last couple of years. It's packed with examples to get you confident writing and refactoring your useEffect code.

In a single afternoon, you'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.

Master useEffect, in a single afternoon.

useEffect By Example's book cover