Preventing Memory Leaks from Uncanceled API Requests
A critical performance issue in React applications occurs when API requests continue running after components unmount, potentially causing memory leaks and state updates on unmounted components.
The Problem: Orphaned API Requests
// ❌ Dangerous - no request cancellation
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
setData(await response.json());
};
fetchData();
}, []);
Why This is Dangerous
- Memory Leaks: Component is gone but request continues
- State Updates on Unmounted Components: Causes React warnings
- Wasted Resources: Network bandwidth and processing
- Race Conditions: Old requests may override newer ones
- Potential Errors: Attempting to update nonexistent state
Correct Implementation Patterns
1. Using AbortController (Modern Fetch API)
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('/api/data', {
signal: controller.signal
});
const data = await response.json();
setData(data);
} catch (error) {
if (error.name !== 'AbortError') {
setError(error.message);
}
}
};
fetchData();
return () => controller.abort(); // Cleanup function
}, []);
2. Using Axios CancelToken
useEffect(() => {
const source = axios.CancelToken.source();
const fetchData = async () => {
try {
const response = await axios.get('/api/data', {
cancelToken: source.token
});
setData(response.data);
} catch (error) {
if (!axios.isCancel(error)) {
setError(error.message);
}
}
};
fetchData();
return () => source.cancel('Component unmounted');
}, []);
3. Mounted Check Pattern (Fallback)
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const data = await response.json();
if (isMounted) setData(data); // Only update if mounted
} catch (error) {
if (isMounted) setError(error.message);
}
};
fetchData();
return () => {
isMounted = false; // Cleanup function
};
}, []);
Best Practices
- Always Clean Up: Every
useEffect
with async operations needs cleanup - Prefer AbortController: Native browser API for fetch cancellation
- Handle Cancel Errors: Specifically catch and ignore abort errors
- Use Loading States: Prevent UI updates during unmount
- Consider Libraries: React Query/SWR handle cancellation automatically
Common Pitfalls
- Forgetting to Cancel:
useEffect(() => {
fetch('/api/data').then(setData); // ❌ No cleanup
}, []);
- Canceling Too Early:
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal });
controller.abort(); // ❌ Cancels immediately
}, []);
- Ignoring AbortError:
try {
// fetch...
} catch (error) {
setError(error); // ❌ Includes AbortError
}
Advanced Patterns
1. Custom Hook with Cancellation
function useCancellableFetch() {
const controllerRef = useRef(new AbortController());
const fetchData = async (url, options = {}) => {
controllerRef.current.abort(); // Cancel previous
controllerRef.current = new AbortController();
try {
const response = await fetch(url, {
...options,
signal: controllerRef.current.signal
});
return await response.json();
} catch (error) {
if (error.name !== 'AbortError') throw error;
}
};
useEffect(() => {
return () => controllerRef.current.abort();
}, []);
return fetchData;
}
// Usage
function MyComponent() {
const fetchData = useCancellableFetch();
useEffect(() => {
fetchData('/api/data').then(setData).catch(setError);
}, [fetchData]);
}
2. Combining with Debouncing
useEffect(() => {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(setResults);
}, 300);
return () => {
controller.abort();
clearTimeout(timeoutId);
};
}, [query]);
3. With React Query (Recommended Solution)
import { useQuery } from 'react-query';
function MyComponent() {
const { data } = useQuery('data', () =>
fetch('/api/data').then(res => res.json())
);
// Automatic cleanup and cancellation
}
Key Takeaways
- AbortController is Your Friend: The modern way to cancel fetch requests
- Cleanup Functions are Mandatory: For any effect with async operations
- Race Conditions Matter: New requests should cancel old ones
- Error Handling is Crucial: Specifically handle cancellation errors
- Consider Higher-Level Solutions: Libraries like React Query simplify this
Remember: Uncanceled API requests are among the most common sources of memory leaks in React applications. Always implement proper cleanup to ensure your application remains performant and error-free.