useEffect must not return anything besides a function, which is used for clean-up
ID: react/useeffect-async-callback
Version Compatibility
| Version | Status | Introduced | Deprecated | Notes |
|---|---|---|---|---|
| 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.
genericWorkarounds
-
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
-
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
-
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:
-
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.
-
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.