Managing loading spinner with multiple simultaneous API requests

I’m working on a React component where I need to handle loading states properly. Right now I have a simple boolean state for tracking loading status:

const [showSpinner, setShowSpinner] = useState(false)

The problem is I have multiple API endpoints that need to be called at the same time:

const fetchUserData = () => {
  setShowSpinner(true)
  makeRequest()
  setShowSpinner(false)
}

const fetchProducts = () => {
  setShowSpinner(true)
  makeRequest()
  setShowSpinner(false)
}

const fetchSettings = () => {
  setShowSpinner(true)
  makeRequest()
  setShowSpinner(false)
}

I trigger all these functions inside useEffect(). The issue is that when any single API call finishes, it sets the loading state to false and hides the spinner. But the other requests might still be running in the background.

What I want is to keep showing the loading spinner until ALL the API calls are completely finished. In my render I’m just doing showSpinner && <LoadingSpinner />

I thought about making separate loading states for each request and then checking (loading1 || loading2 || loading3) && <LoadingSpinner /> but that feels messy and hard to maintain.

What’s a cleaner approach to handle this kind of loading state management?

The Problem: You’re working on a React component that needs to display a loading spinner while multiple API calls are in progress. Your current approach uses a single boolean state variable to control the spinner’s visibility, but this causes the spinner to disappear prematurely when any one of the API calls completes, even if others are still running. You want a cleaner way to manage the loading state so the spinner remains visible until all API calls have finished.

TL;DR: The Quick Fix: Use Promise.allSettled() to wait for all your API calls to complete before hiding the spinner.

:thinking: Understanding the “Why” (The Root Cause): Your original approach uses a single boolean state variable (showSpinner). When any API call finishes (successfully or with an error), the loading state is set to false, causing the spinner to disappear. This is problematic because other API calls might still be in progress. To keep the spinner showing until all asynchronous operations are complete, you need a mechanism that waits for all promises to resolve (or reject). Promise.allSettled() provides exactly that functionality. It allows you to wait for all promises in an array to either fulfill or reject, regardless of their individual success or failure. This ensures the loading spinner remains visible until every API call has completed its work.

:gear: Step-by-Step Guide:

Step 1: Implement Promise.allSettled(): Modify your code to use Promise.allSettled() to wait for all API calls to complete.

const [showSpinner, setShowSpinner] = useState(false);

const fetchAllData = async () => {
  setShowSpinner(true);

  await Promise.allSettled([
    fetchUserData(),
    fetchProducts(),
    fetchSettings()
  ]);

  setShowSpinner(false);
};

useEffect(() => {
  fetchAllData();
}, []); // Empty dependency array ensures this runs only once on mount.

Step 2: Ensure API Calls Return Promises: Verify that your fetchUserData(), fetchProducts(), and fetchSettings() functions all return Promises. If they are using async/await internally, they implicitly return a Promise. If not, you might need to wrap them in a Promise.resolve() or use fetch API directly. For instance:

const fetchUserData = async () => {
  //Existing code using await
  // ...
}

const fetchProducts = () => { //Example of a non async function
    return new Promise((resolve, reject) => {
        //Make your API call here and resolve or reject based on outcome
        setTimeout(() => {resolve("Products Fetched")}, 2000);
    })
}

Step 3: Handle Errors (Optional): While Promise.allSettled() waits for all promises, it doesn’t prevent errors. To handle potential errors, you can access the results from Promise.allSettled():

const fetchAllData = async () => {
  setShowSpinner(true);
  const results = await Promise.allSettled([
    fetchUserData(),
    fetchProducts(),
    fetchSettings()
  ]);

  results.forEach(result => {
    if (result.status === 'rejected') {
      console.error('API call failed:', result.reason);
      // Handle the error appropriately, e.g., display an error message
    }
  });
  setShowSpinner(false);
};

:mag: Common Pitfalls & What to Check Next:

  • Incorrect Promise Handling: Ensure that your API calls correctly return Promises, and handle any errors that may occur. Use try...catch blocks within your individual API functions if necessary for more granular error handling.

  • Unnecessary Re-renders: Ensure that the only thing that is updated in your component when setShowSpinner is invoked is the loading spinner itself. If not, it might cause unnecessary re-renders or unexpected behavior. Consider using React.memo on the component to prevent unneeded re-renders.

  • Concurrent Requests: If you need to manage and handle concurrent API requests effectively, consider employing debouncing or throttling techniques if appropriate to the situation. This can help to avoid unnecessary load on your servers, and improve the overall user experience.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

you could also try react-query or swr - they automatically handle loading states for all your queries. just wrap your api calls and the library does the rest. way less code than tracking state manually, plus you get free caching and refetching.

Skip the complex state management entirely. This is exactly why I automate everything that touches multiple APIs.

Instead of wrestling with counters and refs, set up an automation workflow that handles all your API calls as one coordinated sequence. Your React component just triggers one endpoint, and the automation orchestrates everything behind the scenes.

I dealt with this same mess on a dashboard hitting 8 different services. Managing loading states in React was a nightmare. Now I have one automation flow that calls all APIs, handles retries, and returns everything as a single response. My component went from 50 lines of loading logic to just:

const [loading, setLoading] = useState(false)

const fetchAll = async () => {
  setLoading(true)
  const result = await callAutomatedEndpoint()
  setLoading(false)
}

Bonus: you get proper error handling, retry logic, and can easily add caching or rate limiting without touching your frontend code.

Latenode makes this super straightforward. Build the workflow once, call it from anywhere.

Had this exact problem last year - ended up using a ref to track active requests. useState re-renders mess with timing when you’ve got multiple async operations running.

const loadingCount = useRef(0)
const [isLoading, setIsLoading] = useState(false)

const startLoading = () => {
  loadingCount.current++
  setIsLoading(true)
}

const stopLoading = () => {
  loadingCount.current--
  if (loadingCount.current === 0) {
    setIsLoading(false)
  }
}

Just wrap your API calls with startLoading/stopLoading. The ref stops stale closures from screwing up your count - pure counter approach gave me hell with this. Works great even when requests finish randomly or some fail.

Use a Map or Set to track request IDs instead of counting. I had issues with counters when API calls got cancelled or components unmounted - the count would get out of sync and the spinner would hang forever.

const activeRequests = useRef(new Set())
const [loading, setLoading] = useState(false)

const trackRequest = (requestId, promise) => {
  activeRequests.current.add(requestId)
  setLoading(true)
  
  return promise.finally(() => {
    activeRequests.current.delete(requestId)
    if (activeRequests.current.size === 0) {
      setLoading(false)
    }
  })
}

Just wrap each API call like trackRequest('userData', fetchUserData()). The Set handles duplicates automatically and makes debugging easier since you can see exactly which requests are still pending. Saved me hours debugging race conditions when users navigate away quickly.

honestly just use a counter instead of boolean. increment when request starts, decrement when it finishes. show spinner when counter > 0. way simpler than managing multiple states and scales automatically if you add more api calls later

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.