Axios calls stuck in a pending state in React usually stem from stale closures in useEffect hooks, uncancelled previous requests interfering with new ones, or missing cleanup logic. To resolve this, you must implement an AbortController to cancel outdated requests and ensure your dependency arrays are correctly defined to prevent infinite loops or missed updates.
Understanding the Pending State
When an Axios request remains pending indefinitely, it typically means the browser has sent the HTTP request but has not received a response within the expected timeframe, or the promise was never resolved or rejected due to code flow issues. In React applications, this is often exacerbated by component lifecycle management. If a component unmounts while a request is still active, or if rapid user interactions trigger multiple overlapping requests, the application state can become inconsistent. The UI may show a loading spinner that never disappears because the success or error handlers are bound to a different instance of the component or are blocked by earlier pending promises. This behavior is particularly common when fetching data based on changing props or state variables without proper synchronization mechanisms.
Implementing Request Cancellation with AbortController
The most robust solution to prevent pending calls from stacking up is using the
AbortController API. This allows you to signal intent to cancel a specific request before it completes. By creating an abort controller inside your effect, you can pass its signal to the Axios config. When the component unmounts or when a new request is initiated due to a dependency change, you can call
controller.abort(). This immediately rejects the pending promise, allowing your catch block to handle the cancellation gracefully rather than leaving the request hanging. This approach ensures that only the latest request for a given piece of data remains active, eliminating race conditions where an older, slower response overwrites newer data.
import { useEffect, useRef } from 'react';
import axios from 'axios';
function DataFetcher({ id }) {
const [data, setData] = useState(null);
const abortControllerRef = useRef(null);
useEffect(() => {
// Cancel any previous request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const controller = new AbortController();
abortControllerRef.current = controller;
const fetchData = async () => {
try {
const response = await axios.get(`/api/data/${id}`, {
signal: controller.signal
});
setData(response.data);
} catch (error) {
if (axios.isCancel(error)) {
console.log('Request cancelled');
} else {
console.error('Fetch error:', error);
}
}
};
fetchData();
// Cleanup function runs on unmount or before re-running effect
return () => {
controller.abort();
};
}, [id]);
return {data ? JSON.stringify(data) : 'Loading...'}
;
}
Correcting useEffect Dependency Arrays
Another frequent cause of pending requests is an incorrect dependency array in
useEffect. If you omit dependencies that trigger data fetching, the effect might run only once on mount, ignoring subsequent prop changes. Conversely, including non-primitive objects or functions as dependencies without memoization can cause the effect to run on every render, creating a storm of concurrent requests. Each new request starts, but if they all fail to complete due to network latency or server load, they accumulate in a pending state. Ensure that all variables used inside the fetch URL or headers are included in the dependency array. For complex objects, consider using
useMemo or extracting them into stable references to prevent unnecessary re-runs.
Handling Network Errors and Timeouts
Sometimes the issue is not code-related but environmental. Network interruptions, CORS errors, or server timeouts can leave requests in a pending state if the client does not have a timeout configured. Axios defaults to no timeout, meaning a request could hang indefinitely if the server stops responding. Setting a reasonable timeout value forces Axios to reject the promise after a specified duration, freeing up the UI state. Additionally, always check for network connectivity issues in development tools. If you see requests pending in the Network tab, inspect the status code. A status of 0 often indicates a failed network request or a CORS violation, which requires backend configuration fixes rather than frontend code changes.
Related Resources
For more detailed troubleshooting on configuration issues, refer to our guide on
Setting Base URL with React Axios Not Working: A Tech Support Guide, which covers how improper base URLs can lead to silent failures. Also, check
React App Triggers useEffect Twice: A Tech Support Guide to understand how strict mode double-invocation might contribute to duplicate request states.
Frequently Asked Questions
Why do my requests stay pending even after I navigate away?
They stay pending because the component did not properly clean up the request listener. You must use the
useEffect return function to call
abort() on the controller, ensuring the request is terminated when the component unmounts.
Can I use axios.CancelToken instead of AbortController?
While
axios.CancelToken was the legacy method, it is deprecated. Modern Axios versions support the standard
AbortController via the
signal property, which is the recommended approach for better compatibility and standard compliance.
How do I distinguish between a network error and a cancellation?
Use
axios.isCancel(error) to check if the rejection was intentional. This prevents your error handling logic from treating normal cancellations as critical failures, allowing you to ignore them silently or log them differently.
What causes infinite loops of pending requests?
This usually happens when a state update triggered by a successful request causes the same
useEffect to run again, initiating another request. Ensure your state updates are conditional or that your dependency array accurately reflects what should trigger the fetch.
Is it safe to ignore cancelled request errors?
Yes, cancelled requests are expected behavior during rapid navigation or search input changes. Ignoring them in your catch block prevents console spam and unnecessary error reporting, keeping your logs focused on actual failures.