react official logo

AbortController in React Dashboards to Prevent Ghost Requests

The Hidden Cost of Stale Requests

In high-traffic dashboards, the faster your users move, the more your backend suffers from 'ghost requests.' These are API calls triggered for components that have already been unmounted or views that the user has already navigated away from. Without proper management, these requests lead to race conditions where an old request resolves after a newer one, overwriting your UI with stale data.

While many developers reach for complex state management libraries to solve this, the native AbortController API provides a lightweight, browser-native solution to kill requests the moment they are no longer needed.

The Race Condition Problem

Imagine a user clicking through a list of projects. They click Project A, then immediately click Project B. If Project A's API call is slow and Project B's is fast, the UI will first show Project B's data, then suddenly flip back to Project A when that slow request finally finishes. This creates a jarring and broken user experience.

Implementing AbortController in useEffect

The AbortController consists of two parts: a signal and the abort() method. By passing the signal to the fetch request, you give the browser the ability to terminate the connection instantly. Here is how you implement it within a React cleanup function:

import React, { useState, useEffect } from 'react';

const ProjectDetail = ({ projectId }) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const { signal } = controller;

    const fetchData = async () => {
      try {
        const response = await fetch(`/api/projects/${projectId}`, { signal });
        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Request was cancelled');
        } else {
          setError(err.message);
        }
      }
    };

    fetchData();

    // The Cleanup Function
    return () => {
      controller.abort();
    };
  }, [projectId]);

  if (error) return <div>Error: {error}</div>;
  if (!data) return <div>Loading...</div>;

  return <div>{data.name}</div>;
};

Why This Works

In the example above, the useEffect hook runs every time the projectId changes. Before the new effect starts, React runs the cleanup function of the previous effect. By calling controller.abort(), the browser immediately stops the outgoing fetch request for the old projectId. This ensures that only the most recent request can update the component state.

Handling the AbortError

When a fetch is aborted, it throws an error of type AbortError. It is critical to catch this specifically. You don't want to show an "Error Loading Data" toast notification to the user just because they navigated away. Checking err.name === 'AbortError' allows you to silently ignore these cancellations while still logging or handling genuine network failures.

Scaling to Axios

If you prefer Axios over Fetch, the pattern remains almost identical. Axios supports the AbortSignal natively in its config object. This makes it easy to integrate into existing projects without rewriting your entire data fetching layer.

useEffect(() => {
  const controller = new AbortController();

  axios.get(`/api/data`, { signal: controller.signal })
    .then(res => setData(res.data))
    .catch(err => {
       if (axios.isCancel(err)) return;
       // handle real errors
    });

  return () => controller.abort();
}, [dependency]);

By making AbortController a standard part of your data-fetching strategy, you reduce server load and eliminate the flickering UI bugs that plague complex React dashboards.