Avoiding Race Conditions when Fetching Data with React Hooks
Tags: javascript, react, hooks, webdev
Published: 2020-04-01 12:35:11

---
title: Avoiding Race Conditions when Fetching Data with React Hooks
published: true
description: If you're not careful to clean up your React useEffect hook, you can end up with a race condition! In this post, we'll make sure we appropriately clean up our effects so we don't have this issue.
tags: javascript, react, hooks, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/sd3t0gv3fqz8ajmr5cps.gif
---

The React `useEffect` hook is great for performing side effects in functional components. One common example of this is fetching data. If you're not careful to clean up your effect, however, you can end up with a race condition! In this post, we'll make sure we appropriately clean up our effects so we don't have this race condition issue.

## Setup

In our example app, we are going to fake-load people's profile data when their names are clicked. To help visualize the race condition, we'll create a `fakeFetch` function that implements a random delay between 0 and 5 seconds.

```javascript
const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};
```

## Initial Implementation

Our initial implementation will use buttons to set the current profile. We reach for the `useState` hook to implement this, maintaining the following states:

- `person`, the person selected by the user
- `data`, the data loaded from our fake fetch based on the selected person
- `loading`, whether data is currently being loaded

We additional use the `useEffect` hook, which performs our fake fetch whenever `person` changes.

```javascript
import React, { Fragment, useState, useEffect } from 'react';

const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};

const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);

  useEffect(() => {
    setLoading(true);
    fakeFetch(person).then(data => {
      setData(data);
      setLoading(false);
    });
  }, [person]);

  return (
    <Fragment>
      <button onClick={() => setPerson('Nick')}>Nick's Profile</button>
      <button onClick={() => setPerson('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
        <Fragment>
          <h1>{person}</h1>
          <p>{loading ? 'Loading...' : data}</p>
        </Fragment>
      )}
    </Fragment>
  );
};
export default App;
```

If we run our app and click one of the buttons, our fake fetch loads data as expected.

## Hitting the race condition

The trouble comes when we start switching between people in quick succession. Given the fact that our fake fetch has a random delay, we soon find that our fetch results may be returned out of order. Additionally, our selected profile and loaded data can be out of sync. That's a bad look!

![Data out of sync](https://dev-to-uploads.s3.amazonaws.com/i/sd3t0gv3fqz8ajmr5cps.gif)

What's happening here is relatively intuitive: `setData(data)` within the `useEffect` hook is only called after the `fakeFetch` promise is resolved. Whichever promise resolves last will call `setData` last, regardless of which button was actually called last.

## Canceling previous fetches

We can fix this race condition by "canceling" the `setData` call for any clicks that aren't most recent. We do this by creating a boolean variable scoped within the `useEffect` hook and returning a clean-up function from the `useEffect` hook that sets this boolean "canceled" variable to `true`. When the promise resolves, `setData` will only be called if the "canceled" variable is false.

If that description was a bit confusing, the following code sample of the `useEffect` hook should help.

```javascript
useEffect(() => {
  let canceled = false;

  setLoading(true);
  fakeFetch(person).then(data => {
    if (!canceled) {
      setData(data);
      setLoading(false);
    }
  });

  return () => (canceled = true);
}, [person]);
```

Even if a previous button click's `fakeFetch` promise resolves later, its `canceled` variable will be set to `true` and `setData(data)` will not be executed!

Let's take a look at how our new app functions:

![Data in sync](https://dev-to-uploads.s3.amazonaws.com/i/mvzzorgx4azwknolqg95.gif)

Perfect&mdash;No matter how many times we click different buttons, we will always only see data associated with the last button click.

## Full code

The full code from this blog post can be found below:

```javascript
import React, { Fragment, useState, useEffect } from 'react';

const fakeFetch = person => {
  return new Promise(res => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};

const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);

  useEffect(() => {
    let canceled = false;

    setLoading(true);
    fakeFetch(person).then(data => {
      if (!canceled) {
        setData(data);
        setLoading(false);
      }
    });

    return () => (canceled = true);
  }, [person]);

  return (
    <Fragment>
      <button onClick={() => setPerson('Nick')}>Nick's Profile</button>
      <button onClick={() => setPerson('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
        <Fragment>
          <h1>{person}</h1>
          <p>{loading ? 'Loading...' : data}</p>
        </Fragment>
      )}
    </Fragment>
  );
};
export default App;
```