react hooks_error ai_generated true

useEffect must not return anything besides a function, which is used for clean-up

ID: react/useeffect-async-callback

Also available as: JSON · Markdown
95%Fix Rate
95%Confidence
200Evidence
2019-02-01First Seen

Version Compatibility

VersionStatusIntroducedDeprecatedNotes
18 active

Root Cause

Passing an async function directly to useEffect causes it to return a Promise instead of a cleanup function. React expects either undefined or a cleanup function from the effect callback.

generic

Workarounds

  1. 95% success Define the async function inside useEffect and call it immediately
    useEffect(() => {
      const fetchData = async () => {
        try {
          const response = await fetch('/api/data');
          const data = await response.json();
          setData(data);
        } catch (error) {
          setError(error);
        }
      };
      fetchData();
    }, []);

    Sources: https://react.dev/reference/react/useEffect#fetching-data-with-effects

  2. 95% success Add cleanup with AbortController to cancel pending requests on unmount
    useEffect(() => {
      const controller = new AbortController();
      const fetchData = async () => {
        try {
          const res = await fetch('/api/data', { signal: controller.signal });
          const data = await res.json();
          setData(data);
        } catch (e) {
          if (e.name !== 'AbortError') setError(e);
        }
      };
      fetchData();
      return () => controller.abort();
    }, []);

    Sources: https://react.dev/reference/react/useEffect#fetching-data-with-effects

  3. 95% success Use a data fetching library like React Query / TanStack Query instead of manual useEffect
    import { useQuery } from '@tanstack/react-query';
    
    function Component() {
      const { data, error, isLoading } = useQuery({
        queryKey: ['data'],
        queryFn: () => fetch('/api/data').then(res => res.json()),
      });
      // Handles loading, errors, caching, and cleanup automatically
    }

    Sources: https://tanstack.com/query/latest

Dead Ends

Common approaches that don't work:

  1. Using .then() chains instead of async/await to avoid the async callback 55% fail

    While .then() technically avoids the async return issue, it makes error handling harder and leads to deeply nested promise chains. The IIFE pattern is simpler and more maintainable.

  2. Suppressing the warning with eslint-disable without fixing the code 80% fail

    The warning exists because the async callback returns a Promise as the cleanup function. React will try to call the Promise as a function during cleanup, causing silent failures and potential memory leaks from unclean subscriptions.

Error Chain

Leads to:
Frequently confused with: