BEWARE of React.useEffect Race Condition 🐛 BUGS
Tags: javascript, react
Published: 2020-09-28 20:51:31

---
title: BEWARE of React.useEffect Race Condition 🐛 BUGS
published: true
description: 
tags: javascript, react
canonical_url: https://joel.net/beware-of-reactuseeffect-race-condition-bugs
cover_image: https://cdn.hashnode.com/res/hashnode/image/upload/v1601077846212/iqa6Y_nBv.png?w=1600&h=840&fit=crop&crop=entropy&auto=format&q=60
---

{% youtube SYs5E4yrtpY %}

It is pretty common for React's `useEffect` to introduce **Race Condition Bugs**. This can happen any time you have asynchronous code inside of `React.useEffect`.

## What is a Race Condition Bug?

A race condition can happen when there are two asynchronous processes that will both be updating the same value. In this scenario, it's the last process to complete that ends up updating the value.

This may not be what we want. We might want the last process to be started to update the value.

An example of this is a component that fetches data and then re-renders and re-fetches data.

## Example Race Condition Component

This is an example of a component that could have a **Race Condition Bug**.

```javascript
import { useEffect, useState } from "react";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const [person, setPerson] = useState(null);

    useEffect(() => {
        setPerson(null);

        getPerson(id).then((person) => {
            setPerson(person);
        };
    }, [id]);

    return person ? `${id} = ${person.name}` : null;
}
```

At first glance, there doesn't seem to be anything wrong with this code and that's what can make this bug so dangerous.

`useEffect` will fire every time `id` changes and call `getPerson`. If `getPerson` is started and the `id` changes, a second call to `getPerson` will start.

If the first call finishes before the second call, then it will overwrite `person` with data from the first call, causing a bug in our application.

## AbortController

When using `fetch`, you could use an `AbortController` to manually abort the first request.

NOTE: Later on, **we'll find a simpler way to do this**. This code is just for education purposes.

```javascript
import { useEffect, useRef, useState } from "react";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const [data, setData] = useState(null);
    const abortRef = useRef(null);

    useEffect(() => {
        setData(null);

        if (abortRef.current != null) {
            abortRef.current.abort();
        }

        abortRef.current = new AbortController();

        fetch(`/api/${id}`, { signal: abortRef.current.signal })
            .then((response) => {
                abortRef.current = null;
                return response;
            })
            .then((response) => response.json())
            .then(setData);
    }, [id]);

    return data;
}
```

## Canceling the Previous Request

The `AbortController` isn't always an option for us since some asynchronous code doesn't work with an `AbortController`. So we still need a way to cancel the previous async call.

This is possible by setting a `cancelled` flag inside of `useEffect`. We can set this to `true` when the `id` changes using the `unmount` feature of `useEffect`.

NOTE: Later on, **we'll find a simpler way to do this**. This code is just for education purposes.

```javascript
import { useEffect, useState } from "react";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const [person, setPerson] = useState(null);

    useEffect(() => {
        let cancelled = false;
        setPerson(null);

        getPerson(id).then((person) => {
            if (cancelled) return; // only proceed if NOT cancelled
            setPerson(person);
        };

        return () => {
            cancelled = true; // cancel if `id` changes
        };
    }, [id]);

    return person ? `${id} = ${person.name}` : null;
}
```

## Use React Query

I would not recommend handling the aborting or cancelling manually inside of each component. Instead, you should wrap that functionality inside a React Hook. Fortunately there is a library that has already done that for us.

I would recommend using the [react-query](https://github.com/tannerlinsley/react-query) library. This library will prevent race condition bugs as well as provide some other nice things like caching, retries, etc.

I also like the way [react-query](https://github.com/tannerlinsley/react-query) simplifies the code.

```javascript
import { useQuery } from "react-query";
import { getPerson } from "./api";

export const Race = ({ id }) => {
    const { isLoading, error, data } = useQuery(
        ["person", id],
        (key, id) => getPerson(id)
    );

    if (isLoading) return "Loading...";
    if (error) return `ERROR: ${error.toString()}`;
    return `${id} = ${data.name}`;
}
```

The first argument to [react-query](https://github.com/tannerlinsley/react-query) is the cache key and the 2nd is a function that will be called when there is no cache or the cache is stale or invalid.

# Summary

Race condition bugs can occur when there is an asynchronous call inside of `React.useEffect` and `React.useEffect` fires again.

When using `fetch`, you could abort the request. A`Promise` can be cancelled. But I would recommend against manually writing that code for each component and instead use a library like [react-query](https://github.com/tannerlinsley/react-query).

Subscribe to my newsletter on [joel.net](https://joel.net)

Find me on Twitter [@joelnet](https://twitter.com/joelnet) or YouTube [JoelCodes](https://www.youtube.com/JoelCodes)

Cheers 🍻