Fetching data in React: the case of lost Promises
Tags: webdev, javascript, react, ux
Published: 2022-11-16 08:00:00



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fmzvk0t8tw2jmeptrjnp.png)

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

How would you like to be a bad guy? Some evil genius of frontend development who can write seemingly innocent code, which will pass all the tests and code reviews, but will cause the actual app to behave _weird_. Some random data pops up here and there, search results don’t match the actual query, and navigating between tabs makes you think that your app is drunk.

Or maybe instead you’d rather be the hero 🦸🏻‍♀️️ that stops all of this from happening?

Regardless of the moral path you choose, if all of this sounds interesting, then it’s time to continue the conversation about the fundamentals of data fetching in React. This time let’s talk about Promises: what they are, how they can cause race conditions when fetching data, and how to avoid them.

And if you haven’t read the previous “data fetching fundamentals” article yet, here it is: [How to fetch data in React with performance in mind.](https://www.developerway.com/posts/how-to-fetch-data-in-react)

<h2>What is a Promise</h2>

Before jumping into implementing evil (or heroic) masterplans, let’s remember what is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) and why we need them.

Essentially Promise is a… promise 🙂 When javascript executes the code, it usually does it synchronously: step by step. A Promise is one of the very few available to us ways to execute something asynchronously. With Promises, we can just trigger a task and move on to the next step immediately, without waiting for the task to be done. And the task _promises_ that it will notify us when it’s completed. And it does! It’s very trustworthy.

One of the most important and widely used Promise situations is data fetching. Doesn’t matter whether it’s the actual `fetch` call or some abstraction on top of it like [axios](https://github.com/axios/axios), the Promise behavior is the same.

From the code perspective, it’s just this:

```tsx
console.log('first step'); // will log FIRST

fetch('/some-url') // create promise here
  .then(() => { // wait for Promise to be done
      // log stuff after the promise is done
      console.log('second step') // will log THIRD (if successful)
    }
  )
  .catch(() => {
    console.log('something bad happened') // will log THIRD (if error happens)
  })

console.log('third step') // will log SECOND
```

Basically, the flow is: create a promise `fetch('/some-url')` and do something when the result is available in `.then` or handle the error in `.catch`. That’s it. There are a few more details to know of course to completely master promises, you can read them in [the docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). But the core of that flow is enough to understand the rest of the article.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pt1hd9g7nsgjjh132o57.png)

<h2>Promises and race conditions</h2>

One of the most fun parts of promises is the race conditions they can cause. Check this out: I implemented a very simple app for this article.

{% embed https://codesandbox.io/embed/app-with-race-condition-fzyrj5?codemirror=1&hidedevtools=1&view=preview %}

It has tabs column on the left, navigating between tabs sends a fetch request, and the data from the request is rendered on the right. Try to quickly navigate between tabs in it and enjoy the show: the content is blinking, data appears seemingly at random, and the whole thing is just mind-boggling.

How did this happen? Let’s take a look at the implementation.

We have two components there. One is the root `App` component, it manages the state of the active “page”, and renders the navigation buttons and the actual `Page` component.


```tsx
const App = () => {
  const [page, setPage] = useState("1");

  return (
    <>
      <!-- left column buttons -->
      <button onClick={() => setPage("1")}>Issue 1</button>
      <button onClick={() => setPage("2")}>Issue 2</button>

      <!-- the actual content -->
      <Page id={page} />
    </div>
  );
};
```

`Page` component accepts `id` of the active page as a prop, sends a fetch request to get the data, and then renders it. Simplified implementation (without the loading state) looks like this:

```tsx
const Page = ({ id }: { id: string }) => {
  const [data, setData] = useState({});

  // pass id to fetch relevant data
  const url = `/some-url/${id}`;

  useEffect(() => {
    fetch(url)
      .then((r) => r.json())
      .then((r) => {
        // save data from fetch request to state
        setData(r);
      });
  }, [url]);

  // render data
  return (
    <>
      <h2>{data.title}</h2>
      <p>{data.description}</p>
    </>
  );
};
```

With `id` we determine the `url` from where to fetch data from. Then we’re sending the `fetch` request in `useEffect`, and storing the result data in state - everything is pretty standard. So where does the race condition and that weird behavior come from?

<h2>Race condition reasons</h2>

It all comes down to two things: the nature of Promises and React lifecycle.

From the lifecycle perspective what happens is this:

* `App` component is mounted
* `Page` component is mounted with the default prop value “1”
* `useEffect` in `Page` component kicks in for the first time

Then the nature of Promises comes into effect: `fetch` within `useEffect` is a promise, asynchronous operation. It sends the actual request, and then React just moves on with its life without waiting for the result. After ~2 seconds the request is done, `.then` of the promise kicks in, within it we call setData to preserve the data in the state, the `Page` component is updated with the new data, and we see it on the screen.

If after everything is rendered and done I click on the navigation button, we’ll have this flow of events:

* `App` component changes its state to another page
* State change triggers re-render of `App` component
* Because of that, `Page` component will re-render as well (here is a helpful guide with more links if you’re not sure why: [React re-renders guide: everything, all at once](https://www.developerway.com/posts/react-re-renders-guide))
* `useEffect` in `Page` component has a dependency on `id`, `id` has changed, `useEffect` is triggered again
* `fetch` in `useEffect` will be triggered with the new `id`, after ~2 seconds setData will be called again, `Page` component updates and we’ll see the new data on the screen

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qzyapuljpezl9iqqq58z.png)

But what will happen if I click on a navigation button and the `id` changes while the first fetch is in progress and hasn’t finished yet? Really cool thing!

* `App` component will trigger re-render of `Page` again
* `useEffect` will be triggered again (id has changed!)
* `fetch` will be triggered again, and React will continue with its business as usual
* then _the first fetch will finish_. It still has the reference to `setData` of the exact same `Page` component (remember - it just updated, so the component is still the same)
* `setData` after the first fetch will be triggered, `Page` component will update itself with the data from the **first** fetch
* then the second fetch finishes. It was still there, hanging out in the background, as any promise would do. That one also has the reference to exactly the same setData of the same `Page` component, it will be triggered, `Page` will again update itself, only this time with the data from the **second** fetch.

Boom 💥, race condition! After navigating to the new page we see the flash of content: the content from the first finished fetch is rendered, then it’s replaced by the content from the second finished fetch.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ybljvkzbuo2c7m4n6bl.png)

This effect is even more interesting if the second fetch finishes before the first fetch. Then we’ll see first the correct content of the next page, and then it will be replaced by the incorrect content of the previous page.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zv22bxmdj5fwfk2hevvf.png)

Check out the example below. Wait until everything is loaded for the first time, then navigate to the second page, and quickly navigate back to the first page.

{% embed https://codesandbox.io/embed/app-without-race-condition-reversed-yuoqkh?codemirror=1&hidedevtools=1&view=preview %}

Okay, the evil deed is done, the code is innocent, but the app is broken. Now what? How to solve it?

<h2>Fixing race conditions: force re-mounting</h2>

The first one is not even a solution per se, it’s more of an explanation of why those race conditions don’t actually happen that often, and why we usually don’t see them during regular page navigation.

Imagine instead of the implementation above we'd have something like this:


```tsx
const App = () => {
  const [page, setPage] = useState('issue');

  return (
    <>
      {page === 'issue' && <Issue />}
      {page === 'about' && <About />}
    </>
  )
}
```

No passing down props, `Issue` and `About` components have their own unique urls from which they fetch the data. And the data fetching happens in `useEffect` hook, exactly the same as before:

```tsx
const About = () => {
  const [about, setAbout] = useState();

  useEffect(() => {
    fetch("/some-url-for-about-page")
      .then((r) => r.json())
      .then((r) => setAbout(r));
  }, []);
  ...
}
```

This time there is no race condition while navigating. Navigate as many times and as fast as you want: the app behaves normally.

{% embed https://codesandbox.io/embed/issue-and-about-no-bug-5udo04?codemirror=1&hidedevtools=1&view=preview %}


Why? 🤔

The answer is here: `{page === ‘issue' && <Issue />}`.  `Issue` and `About` page are not re-rendered when `page` value changes, they _re-mounted_. When value changes from `issue` to `about`, the `Issue` component unmounts itself, and `About` component is mounted on its place.

What is happening from the fetching perspective is this:

* the `App` component renders first, mounts the `Issue` component, data fetching there kicks in
* when I navigate to the next page while the fetch is still in progress, the `App` component _unmounts_ `Issue` page and mounts `About` component instead, it kicks off its own data fetching

And when React unmounts a component, it means it's gone. Gone completely, disappears from the screen, no one has access to it, everything that was happening within including its state is lost. Compare it with the previous code, where we wrote `<Page id={page} />`. This `Page` component was never unmounted, we were just re-using it and its state when navigating.

So back to the unmounting situation. When the `Issue`'s fetch request finishes while I’m on `About` page, the `.then` callback of the `Issue` component will try to call its `setIssue` state. But the component is gone, from React perspective it doesn’t exist anymore. So the promise will just die out, and the data it got will just disappear into the void.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fnotqcmk7afvf70mytkg.png)


By the way, do you remember that scary warning _“Can’t perform a React state update on an unmounted component”_? It used to appear in exactly those situations: when an asynchronous operation like data fetching finishes after the component is gone already. “Used to”, since it’s gone as well. Was removed quite recently: [Remove the warning for setState on unmounted components by gaearon · Pull Request #22114 · facebook/react](https://github.com/facebook/react/pull/22114). Quite an interesting read on the reasons for those who like to have all the details.

Anyway. In theory, this behavior can be applied to solve the race condition in the original app: all we need is to force Page component to re-mount on navigation. We can use [“key” attribute](https://beta.reactjs.org/learn/preserving-and-resetting-state#option-2-resetting-state-with-a-key) for this:

```tsx
<Page id={page} key={page} />
```

[See example in codesandbox](https://codesandbox.io/s/app-without-race-condition-twv1sm?file=/src/App.tsx)

⚠️ This is not a solution I would recommend for the race conditions problem, too many caveats: performance might suffer, unexpected bugs with focus and state, unexpected triggering of `useEffect` down the render tree. It's more like sweeping the problem under the rug. There are better ways to deal with race conditions (see below). But it can be a tool in your arsenal in certain cases if used carefully.

If you never used `key` before, not sure why all those bugs will happen, and want to understand how it works, this article might be useful: [React key attribute: best practices for performant lists](https://www.developerway.com/posts/react-key-attribute)

<h2>Fixing race conditions: drop incorrect result</h2>


A much more gentle way to solve race conditions, instead of nuking the entire `Page` component from existence, is just to make sure that the result coming in `.then` callback matches the id that is currently “acitve”.

If the result returns the “id” that was used to generate the `url`, we can just compare them. And if they don’t match - ignore them. The trick here is to escape React lifecycle and locally scoped data in functions and get access to the “latest” id inside all iterations of `useEffect`, even the “stale” ones. [React ref](https://beta.reactjs.org/learn/referencing-values-with-refs#adding-a-ref-to-your-component) is perfect for this:

```tsx
const Page = ({ id }) => {
  // create ref
  const ref = useRef(id);

  useEffect(() => {
    // update ref value with the latest id
    ref.current = id;

    fetch(`/some-data-url/${id}`)
      .then((r) => r.json())
      .then((r) => {
        // compare the latest id with the result
        // only update state if the result actually belongs to that id
        if (ref.current === r.id) {
          setData(r);
        }
      });
  }, [id]);
}
```

[See example in codesandbox](https://codesandbox.io/s/app-with-race-condition-fixed-with-id-and-ref-jug1jk?file=/src/App.tsx)

Your results don’t return anything that identifies them reliably? No problem, we can just compare `url` instead:

```tsx
const Page = ({ id }) => {
  // create ref
  const ref = useRef(id);

  useEffect(() => {
    // update ref value with the latest url
    ref.current = url;

    fetch(`/some-data-url/${id}`)
      .then((result) => {
        // compare the latest url with the result's url
        // only update state if the result actually belongs to that url
        if (result.url === ref.current) {
          result.json().then((r) => {
            setData(r);
          });
        }
      });
  }, [url]);
}
```

[See example in codesandbox](https://codesandbox.io/s/app-with-race-condition-fixed-with-url-and-ref-whczob?file=/src/App.tsx)

<h2>Fixing race conditions: drop all previous results</h2>


Don’t like the previous solution or think that using ref for something like this is weird? No problem, there is another way. `useEffect` has something that is called “cleanup” function, where we can clean up stuff like subscriptions. Or in our case active fetch requests.

The syntax for it looks like this:

```tsx
// normal useEffect
useEffect(() => {

  // "cleanup" function - function that is returned in useEffect
  return () => {
    // clean something up here
  }
// dependency - useEffect will be triggered every time url has changed
}, [url]);
```

[The cleanup function](https://beta.reactjs.org/apis/react/useEffect#my-cleanup-logic-runs-even-though-my-component-didnt-unmount) is run after a component is unmounted, or _before every useEffect call with changed dependencies_. So the order of operations during re-render will look like this:

* url changes
* “cleanup” function is triggered
* actual content of `useEffect` is triggered

This, and the nature of javascript’s functions and [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) allows us to do this:

```tsx
useEffect(() => {
  // local variable for useEffect's run
  let isActive = true;

  // do fetch here

  return () => {
    // local variable from above
    isActive = false;
  }
}, [url]);
```

We’re introducing a local boolean variable `isActive` and setting it to `true` on `useEffect` run and to `false` on cleanup. The function in `useEffect` is re-created on every re-render, so the `isActive` for the latest `useEffect` run will always reset to `true`. But! The “cleanup” function, which runs before it, still has access to the scope of the _previous function_, and it will reset it to `false`. This is how [javascript closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) work.

And `fetch` Promise, although async, still exists only within that closure and has access only to the local variables of the `useEffect` run that started it. So when we check the `isActive` boolean in `.then` callback, only the latest run, that one that hasn’t been cleaned up yet, will have the variable set to `true`. So all we need now is just check whether we’re in the active closure, and if yes - set state. If no - do nothing, the data will just again disappear into the void.

```tsx
useEffect(() => {
    // set this closure to "active"
    let isActive = true;

    fetch(`/some-data-url/${id}`)
      .then((r) => r.json())
      .then((r) => {
        // if the closure is active - update state
        if (isActive) {
          setData(r);
        }
      });

    return () => {
      // set this closure to not active before next re-render
      isActive = false;
    }
  }, [id]);
```

[See example in codesandbox](https://codesandbox.io/s/app-with-race-condition-fixed-with-cleanup-4du0wf?file=/src/App.tsx)

<h2>Fixing race conditions: cancel all previous requests</h2>


Feeling that dealing with javascript closures in the context of React lifecycle makes your brain explode? I’m with you, sometimes thinking about all of this gives me a headache. But not to worry, there is another option to solve the problem.

Instead of cleaning up or comparing results, we can just cancel all the previous requests. If they never finish, the state update with obsolete data will never happen, and the problem just won’t exist. We can use [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) for this.

It’s as simple as creating `AbortController` in `useEffect` and calling `.abort()` in the cleanup function.

```tsx
useEffect(() => {
    // create controller here
    const controller = new AbortController();

    // pass controller as signal to fetch
    fetch(url, { signal: controller.signal })
      .then((r) => r.json())
      .then((r) => {
        setData(r);
      });

    return () => {
      // abort the request here
      controller.abort();
    };
  }, [url]);
```

So on every re-render the request in progress will be cancelled and the new one will be the only one allowed to resolve and set state.

Aborting a request in progress will make the promise reject, so you’d want to catch errors to get rid of the scary warnings in the console. But handling Promise rejections properly is a good idea regardless of AbortController, so it’s something you’d want to do with any strategy. Rejecting because of AbortController will give a specific type of error, so it will be easy to exclude it from regular error handling.

```tsx
fetch(url, { signal: controller.signal })
  .then((r) => r.json())
  .then((r) => {
    setData(r);
  })
  .catch((error) => {
    // error because of AbortController
    if (error.name === 'AbortError') {
      // do nothing
    } else {
      // do something, it's a real error!
    }
  });
```

[See example in codesandbox](https://codesandbox.io/s/app-with-race-condition-fixed-with-abort-controller-6u0ckk?file=/src/App.tsx)

<h2>Does Async/await change anything?</h2>

Nope, not really. Async/await is just a nicer way to write exactly the same promises. It just turns them into “synchronous” functions from the execution flow perspective but doesn’t change their asynchronous nature. Instead of:

```tsx
fetch('/some-url')
  .then(r => r.json())
  .then(r => setData(r));
```

we’d write:

```tsx
const response = await fetch('/some-url');
const result = await response.json();
setData(result);
```

Exactly the same app implemented with async/await instead of “traditional” promises will have exactly the same race condition. [Check it out in the codesandbox](https://codesandbox.io/s/app-with-race-condition-async-away-q39lgi?file=/src/App.tsx). And all the solutions and reasons from the above apply, just syntax will be slightly different.

---

That’s enough promises for one article I think. Hope you found it useful and never will introduce a race condition into your code. Or if someone tries to do it, you’ll catch them in the act.

And check out the previous article on data fetching in React, if you haven’t yet: [How to fetch data in React with performance in mind](https://www.developerway.com/posts/how-to-fetch-data-in-react). It has more fundamentals and core concepts that are essential to know when dealing with data fetching on the frontend.


---

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.