Frontend Development 10 min read

How to Prevent Race Conditions in React Data Fetching

This article explains what race conditions are in front‑end web development, demonstrates the issue with a React demo fetching articles, and walks through step‑by‑step solutions using custom hooks, useEffect cleanup, and AbortController to safely cancel outdated requests and avoid stale data rendering.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
How to Prevent Race Conditions in React Data Fetching

Introduction Race conditions, translated from the English term "race conditions," occur when multiple asynchronous requests compete and the first request to finish does not necessarily correspond to the first request sent. In front‑end web development, this often appears when fetching data and rendering it in the browser.

Fetching Data

Below is a small demo that fetches article data and renders it on the page.

App.tsx
<code>import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Article from './Article';

function App() {
  return (
    <Routes>
      <Route path="/articles/:articleId" element={<Article />} />
    </Routes>
  );
}

export default App;</code>
Article.tsx
<code>import React from 'react';
import useArticleLoading from './useArticleLoading';

const Article = () => {
  const { article, isLoading } = useArticleLoading();

  if (!article || isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <p>{article.id}</p>
      <p>{article.title}</p>
      <p>{article.body}</p>
    </div>
  );
};

export default Article;</code>

In the

Article

component the data request is encapsulated in a custom hook

useArticleLoading

. The component displays either the fetched data or a loading state.

useArticleLoading.tsx
<code>import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';

interface Article {
  id: number;
  title: string;
  body: string;
}

function useArticleLoading() {
  const { articleId } = useParams<{ articleId: string }>();
  const [isLoading, setIsLoading] = useState(false);
  const [article, setArticle] = useState<Article | null>(null);

  useEffect(() => {
    setIsLoading(true);
    fetch(`https://get.a.article.com/articles/${articleId}`)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        return Promise.reject();
      })
      .then((fetchedArticle: Article) => {
        setArticle(fetchedArticle);
      })
      .finally(() => {
        setIsLoading(false);
      });
  }, [articleId]);

  return { article, isLoading };
}

export default useArticleLoading;</code>

The hook manages the loading state and the data request.

Race‑Condition Scenario

Consider the following sequence:

Visit

/articles/1

– the browser starts a request for article 1, but the network fails, so the response never arrives.

Immediately navigate to

/articles/2

– the browser starts a request for article 2, which succeeds quickly and renders.

Later the request for article 1 finally returns, and its data overwrites the current view, causing the page to show article 1 instead of article 2.

This happens because network latency is unpredictable; the first request does not guarantee the first response.

Solution Using a Cancellation Flag

One simple fix is to ignore responses that are no longer needed. In React this can be done with a flag inside

useEffect

:

useArticlesLoading.tsx
<code>useEffect(() => {
  let didCancel = false;
  setIsLoading(true);
  fetch(`https://get.a.article.com/articles/${articleId}`)
    .then(response => {
      if (response.ok) {
        return response.json();
      }
      return Promise.reject();
    })
    .then((fetchedArticle: Article) => {
      if (!didCancel) {
        setArticle(fetchedArticle);
      }
    })
    .finally(() => {
      setIsLoading(false);
    });
  return () => {
    didCancel = true;
  };
}, [articleId]);</code>

The cleanup function sets

didCancel

to

true

, preventing

setArticle

from running for stale requests.

AbortController Solution

While the flag approach works, it still lets the browser finish the request, wasting resources.

AbortController

can actively cancel the request.

<code>useEffect(() => {
  const abortController = new AbortController();
  setIsLoading(true);
  fetch(`https://get.a.article.com/articles/${articleId}`, {
    signal: abortController.signal,
  })
    .then(response => {
      if (response.ok) {
        return response.json();
      }
      return Promise.reject();
    })
    .then((fetchedArticle: Article) => {
      setArticle(fetchedArticle);
    })
    .catch(() => {
      // handle abort or other errors
    })
    .finally(() => {
      setIsLoading(false);
    });
  return () => {
    abortController.abort();
  };
}, [articleId]);</code>

Calling

abortController.abort()

terminates the request, freeing resources. Errors from the abort can be caught and handled separately.

Stopping Other Promises

AbortController

can also be used to cancel generic promises. Example:

<code>function wait(time: number, signal?: AbortSignal) {
  return new Promise<void>((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      resolve();
    }, time);
    signal?.addEventListener('abort', () => {
      clearTimeout(timeoutId);
      reject();
    });
  });
}

const abortController = new AbortController();
setTimeout(() => abortController.abort(), 1000);

wait(5000, abortController.signal)
  .then(() => console.log('5 seconds passed'))
  .catch(() => console.log('Waiting was interrupted'));
</code>

This demonstrates how a signal can be passed to any asynchronous operation to make it cancellable.

AbortController Compatibility

Compatibility chart (excluding IE) shows that modern browsers fully support

AbortController

:

AbortController compatibility
AbortController compatibility

Conclusion

The article discussed race conditions in React, explained why they occur, and presented two main mitigation strategies: a simple cancellation flag and the more robust

AbortController

. It also showed how

AbortController

can be applied to other asynchronous tasks, encouraging developers to choose the solution that best fits their project.

FrontendreactuseEffectrace conditionCustom Hookdata-fetchingabortcontroller
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.