Try @eslint-react/kit@beta โ†’
logoESLint React

no-leaked-fetch

Enforces that every 'fetch' in a component or custom hook has a corresponding 'AbortController' abort in the cleanup function.

This is an evaluation implementation and may contain false positives or negatives that have not yet been fully audited. Review each report carefully before applying fixes.

Full Name in eslint-plugin-react-web-api

react-web-api/no-leaked-fetch

Full Name in @eslint-react/eslint-plugin

@eslint-react/web-api-no-leaked-fetch

Features

๐Ÿงช

Presets

web-api recommended recommended-typescript recommended-type-checked strict strict-typescript strict-type-checked

Rule Details

Starting a fetch within the setup function of useEffect / useLayoutEffect / useInsertionEffect without passing an AbortSignal and aborting it in cleanup can lead to responses resolving after the component unmounts or after the effect re-runs on a dependency change. If the resolved handler calls setState or writes to a ref the component reads, the UI will display data for a component instance that no longer exists, or overwrite fresh data with stale data.

Common Violations

Invalid

import { useEffect, useState } from "react";

function UserProfile({ id }: { id: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/user/${id}`)
    // ^^^ A 'fetch' started in 'useEffect' must be aborted in the cleanup function.
      .then((r) => r.json())
      .then(setUser);
  }, [id]);

  return user ? <Profile user={user} /> : null;
}

Valid

import { useEffect, useState } from "react";

function UserProfile({ id }: { id: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const ctrl = new AbortController();
    fetch(`/api/user/${id}`, { signal: ctrl.signal })
      .then((r) => r.json())
      .then(setUser);
    return () => ctrl.abort();
  }, [id]);

  return user ? <Profile user={user} /> : null;
}

Troubleshooting

Why is this pattern problematic?

An in-flight fetch does not know its caller has unmounted. When the response resolves, the .then chain runs regardless. If that chain writes state, React logs "Can't perform a state update on an unmounted component" (pre-18) or silently overwrites fresh data (post-18, once the component remounts with new props).

What are the alternatives?

  • AbortController (recommended, canonical React guidance).
  • TanStack Query / SWR: they manage this for you inside their API boundary; code using them exclusively does not need this rule.
  • isMounted ref guard: technically prevents UI corruption but wastes network and doesn't trigger on React's strict-mode double-invoke. Rule does not recognize this pattern as a cleanup.

Resources

Further Reading


See Also

On this page