skip to Main Content

In my app I load data from an api, sometimes there is more than one request necessary (i.e. loading the main data and loading the options of a select field).

My idea is to have a global state in a context (an array in this case), where I add an unique string before the api request and when the request has finished and has returned its data, this unique string is removed from the array. As long as the array length is greater than zero, the loading component is shown and if there are two different api requests that take different times, the loading component isn’t removed until the last request has ended.

My problem is, that the loading component mostly is still shown, even if the "stop function" has removed the unique string and the array is empty.

This is my code so far:

ContextProvider (./contexts/LoadingContext.jsx):

'use strict';

import React, {
    createContext,
    useContext,
    useState,
} from 'react';

const LoadingContext = createContext({
    startLoading: (id) => {},
    stopLoading: (id) => {},
    isLoading: false,
});


export const useLoading = () => useContext(LoadingContext);

export const LoadingProvider = ({ children }) => {

    const [loading, setLoading] = useState([]);

    const startLoading = (id) => {
        setLoading([...loading, id]);
    };

    const stopLoading = (id) => {
        const _loading = [...loading];
        const index = _loading.indexOf(id);
        if(index !== -1) {
            _loading.splice(index, 1);
            setLoading(_loading);
        }
    };

    return (
        <LoadingContext.Provider value={{
            startLoading: startLoading,
            stopLoading: stopLoading,
            isLoading: loading.length > 0,
        }}>
            {children}
        </LoadingContext.Provider>
    );
}

GlobalLoadingBackdrop (./GlobalLoadingBackdrop.jsx):

'use strict';

import React from 'react';
import Backdrop from '@mui/material/Backdrop';
import CircularProgress from '@mui/material/CircularProgress';
import { useLoading } from './contexts/LoadingContext';

export const GlobalLoadingBackdrop = () => {

    const { isLoading} = useLoading();

    return (
        <Backdrop
            sx={{
                color: '#fff',
                zIndex: (theme) => theme.zIndex.modal + 1,
            }}
            open={isLoading}
        >
            <CircularProgress color={'inherit'} />
        </Backdrop>
    );
}

Usage somewhere in a functional component:

const { startLoading, stopLoading } = useLoading();

try {
    // start loading
    startLoading('GET/some/api/request');
    // call api
    const response = await axios.get('/some/api/request');
    // check response
    if(response.hasOwnProperty('error') && response.error !== null) {
        throw new Error(response.errorMessage);
    }
    // do somthing with the data
} catch (e) {
    console.error(e);
} finally {
    // stop loading
    stopLoading('GET/some/api/request');
}

Any help or idea how to solve the problem will be appreciated…

2

Answers


  1. Dans la fonction stopLoading, utilisez filter pour créer un nouveau tableau sans muter l’original, plutôt que d’utiliser splice.

    const stopLoading = (id) => {
      setLoading((prevLoading) => prevLoading.filter((loadingId) => loadingId !== id));
    };

    Déplacez l’appel à stopLoading dans la clause finally de votre bloc try-catch. Cela garantit que la fonction stopLoading est toujours appelée, même en cas d’erreur non gérée pendant la requête API.

    const { startLoading, stopLoading } = useLoading();
    
    try {
      // start loading
      startLoading('GET/some/api/request');
      // appel à l'API
      const response = await axios.get('/some/api/request');
      // vérifier la réponse
      if (response.hasOwnProperty('error') && response.error !== null) {
        throw new Error(response.errorMessage);
      }
      // faire quelque chose avec les données
    } catch (e) {
      console.error(e);
    } finally {
      // stop loading (placé dans la clause finally pour s'assurer qu'il est toujours appelé)
      stopLoading('GET/some/api/request');
    }
    Login or Signup to reply.
  2. I think the error might be caused by the way you are setting and removing the ids from the loading array. You are using the spread operator to create a new array, but you are also mutating the original array with splice. This might cause some inconsistency and confusion for React.

    A better way to update the loading array is to use a functional update, which means passing a function to setLoading that receives the previous array as an argument, and returns a new array based on it. This way, you always use the latest array value. For example:

    const startLoading = (id) => {
      setLoading((prevLoading) => [...prevLoading, id]);
    };
    
    const stopLoading = (id) => {
      setLoading((prevLoading) => prevLoading.filter((item) => item !== id));
    };
    

    And I think you can use useTransition hook which is newly introduced in React 18 for loading.

    To learn more, please see documentation.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search