TypeScript  

Easy way to Handle cancellation of API Requests in React with AbortSignal (TypeScript)

Introduction

When building React applications with TypeScript, fetching data from APIs is a kind of default pattern. APIs enable loosely coupled architectures, which makes your app more modular and scalable. However, handling asynchronous requests introduces a challenge:

  1. What happens if a component unmounts before its request completes?
  2. Or if you need to cancel a request manually and trigger a new one?

A practical example is modern chat applications. Take ChatGPT, for instance, you’ve probably noticed the “Stop generating” button, which immediately cancels the ongoing request so the model stops responding. This is exactly the kind of behavior we want to replicate in our own apps.

ChatGPT

Without proper cancellation, you risk:

  • Wasted bandwidth from unnecessary API calls
  • React warns about state updates on unmounted components
  • Poor user experience, as outdated requests may overwrite fresh data

The solution is the AbortController + AbortSignal API, built into modern browsers. It gives you control over request lifecycles, allowing you to abort ongoing fetches safely.

In this tutorial, we’ll walk through:

  1. Setting up an API call in React
  2. Integrating AbortController for cancellation
  3. Handling aborted requests
  4. Verifying cancellation in the browser’s Network tab

Step 1. Start with a Basic Fetch

First, let’s fetch some dummy data when the component mounts:

import { useEffect, useState } from "react";

export default function DataFetcher() {
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts/1")
      .then((res) => res.json())
      .then(setData);

    // No cancellation here yet
  }, []);

  if (!data) return <p>Loading...</p>;
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

This works fine, but there’s a catch: The request will always finish, even if you close or navigate away from the component.

  • If the component unmounts before the fetch resolves, React might warn about updating state on an unmounted component.
  • You also have no way to manually cancel the request if the user decides to stop it.

Step 2. Create an AbortController

In React, we usually manage cleanup inside useEffect.

That’s where we’ll create the controller:

useEffect(() => {
  const controller = new AbortController();   // Create
  const { signal } = controller;              // Extract the signal

  fetch("https://jsonplaceholder.typicode.com/posts/1", { signal })
    .then((res) => res.json())
    .then(setData)
    .catch((err) => {
      // Catch aborts separately
      if (err.name === "AbortError") { // Catch Abort
        console.log("Fetch aborted!");
      } else {
        console.error("Fetch failed:", err);
      }
    });

  return () => {
    controller.abort();  // Cleanup on unmount
  };
}, []);

Here’s what’s happening:

  1. Create controllernew AbortController()
  2. Pass signal{ signal } into fetch
  3. Catch cancellation: If err.name === "AbortError"
  4. Abort on unmount: Return cleanup in useEffect

Step 3. Adding a Cancel Button

In real apps, you may want the user to cancel an API request.

Here’s a full flow with Start and Cancel buttons:

import { useState } from "react";

export default function CancelableFetch() {
  const [controller, setController] = useState<AbortController | null>(null);

  // Stores the AbortController so we can cancel later
  const [loading, setLoading] = useState(false);
  const [status, setStatus] = useState<string>("Idle");
  const [data, setData] = useState<any>(null);

  const startFetch = () => {
    // 1: Create a new AbortController before starting the request
    const newController = new AbortController();
    const signal = newController.signal;
    // Save it in state so we can access it later when canceling
    setController(newController);

    setLoading(true);
    setStatus("Fetching...");

    // 2: Pass the AbortSignal (signal) into fetch
    fetch("https://httpbin.org/delay/5", {
      signal: signal,
    })
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setStatus("Success");
      })
      .catch((err) => {
        // 3️: If the request was canceled, fetch throws an AbortError
        if (err.name === "AbortError") {
          setStatus("Request canceled by user");
        } else {
          setStatus("Error fetching data");
        }
      })
      .finally(() => {
        setLoading(false);
        setController(null); // clear controller after request finishes/cancels
      });
  };

  const cancelFetch = () => {
    // 4️: Call abort() on the controller → this triggers the AbortSignal
    controller?.abort();

    // Reset state
    setController(null);
    setLoading(false);
  };

  return (
    <div style={{ padding: "1rem", fontFamily: "sans-serif" }}>
      <h2>AbortController Demo</h2>

      <div style={{ marginBottom: "1rem" }}>
        <button onClick={startFetch} disabled={loading} style={{ marginRight: "0.5rem" }}>
          Start Fetch
        </button>

        <button onClick={cancelFetch} disabled={!controller}>
          Cancel Fetch
        </button>
      </div>

      <p><strong>Status:</strong> {status}</p>

      {loading && <p>⏳ Waiting for response... (you can cancel)</p>}

      {data && (
        <pre
          style={{
            background: "black",
            color: "lime",
            padding: "1rem",
            borderRadius: "8px",
            maxWidth: "600px",
            overflowX: "auto",
          }}
        >
          {JSON.stringify(data, null, 2)}
        </pre>
      )}
    </div>
  );
}

Step 4. Verify in DevTools

To actually see cancellation:

  1. Open your React app.
  2. Open DevTools > Network tab.
  3. Click Start Fetch. You’ll see a request in progress.
  4. Before it finishes, click Cancel Fetch.
  5. The request will show (canceled in red color with 0B in size) in the Network tab.
  6. Click Start Fetch again, and this time let it process. The request will process with 1.1kb in size in the Network tab.

Demo

Step 5. Where Each Piece Belongs (React Flow)

  • Create AbortController: Inside your function or useEffect, right before you start fetch.
  • Pass signal: Into fetch (or other APIs that support it).
  • Catch cancellation: Inside .catch, check err.name === "AbortError".
  • Abort request: Either:
    • In useEffect cleanup (return () => controller.abort()), or
    • On a user action (like a Cancel button).

This is the proper flow in React:

create > attach > handle > cleanup.

Summary

So what do we know and what did we learn, we explore how to safely cancel API requests in React applications using TypeScript with the help of the AbortController + AbortSignal browser APIs.

  1. Why cancellation matters
    • Prevent wasted bandwidth
    • Avoid React warnings about unmounted components
    • Ensure a smooth user experience (no outdated data overwriting fresh data)
  2. Step-by-step implementation
    • 1: Basic fetch request without cancellation (the naive approach).
    • 2: Using AbortController inside useEffect for cleanup when components unmount.
    • 3: Adding a Start / Cancel button flow to allow manual request cancellation.
    • 4: Verifying cancellation behavior using the browser’s Network tab in DevTools.
    • 5: Best practices for React apps — create → attach → handle → cleanup.

Key Takeaways

  • AbortController gives you control over request lifecycles.
  • You can cancel requests both automatically (when a component unmounts) and manually (on user action).
  • The cancellation flow in React is simple and predictable: create controller > pass signal > handle abort > cleanup..