1

I was trying to implement a global loading screen when fetching data from an API, I read this answer and tried to implemented something similar.

loading.provider.js

import { createContext, useContext, useState } from "react";

const LoadingContext = createContext({
  loading: false,
  setLoading: null
});

export function LoadingProvider({ children }) {
  const [loading, setLoading] = useState(false);
  const value = { loading, setLoading };
  return (
    <LoadingContext.Provider value={value}>{children}</LoadingContext.Provider>
  );
};

export function useLoading() {
  const context = useContext(LoadingContext);
  if (!context) {
    throw new Error('useLoading must be used within LoadingProvider');
  }

  return context;
};

app.js

The LoadingScreen has been added to this screen:

import Layout from '@/components/Layout/Layout';
import { LoadingProvider, useLoading } from '@/Providers/loading.provider';
import LoadingScreen from "./LoadingScreen";

export default function App({ Component, pageProps }) {
  const { loading } = useLoading();

  return (
    <>
      <CssBaseline />
      <LoadingProvider>
        <AppStateProvider>
          { loading && <LoadingScreen loading={true} bgColor='#fff' spinnerColor={'#00A1FF'} textColor='#676767'></LoadingScreen> }
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </AppStateProvider>
      </LoadingProvider>
    </>
  );
};

I have a Layout component which holds a SideMenu, top AppBar and the main components:

Layout.js

export default function Layout({ children }) {
  return (
    <Fragment>
      <Box sx={{ display: 'flex' }}>
        <AppBar />
        <SideMenu />
        <Box component="main" sx={mainContentWrapper}>
          <div className={styles.container}>
            <main className={styles.main}>
              {children}
            </main>
          </div>
        </Box>
      </Box>
    </Fragment>
  );
}

Now I was trying to show it from a page ([identifier].js):

import { useLoading } from "@/Providers/loading.provider";

export default function MainPage({ response }) {
  const { loading, setLoading } = useLoading();
  const btnClickedAll = async (data) => {
    setLoading(true);
  };

  return (
    <Fragment>
      <Button sx={{ ml: 2 }} key={'btnAll'} onClick={btnClickedAll} variant="contained">All</Button>,
    </Fragment>
  );
}

loading is being called after setting it with setLoading(true) but the state is not changing inside app.js. Can someone help me and point me to the right direction?


Edited: Made some changes in code according to @DustInComp suggestion.

ytpm
  • 4,962
  • 6
  • 56
  • 113
  • 3
    You named the value `loading`, not `isLoading`. Fix it in Layout.js – DustInComp Feb 13 '23 at 10:15
  • Amazing, thank you, I have another question, I moved the implementation from `Layout.js` into `app.js` and it stoped working, you have an idea what may cause it? – ytpm Feb 13 '23 at 11:01
  • You are trying to read the `loading` state before it is set. You need to call `useLoading` anywhere within the `` component. This is why you have the `if(!context)` inside the `useLoading` function definition. – GoodMan Feb 13 '23 at 13:10

1 Answers1

1

The reason is that your LoadingProvider is inside of your app.js, and only children inside off the LoadingProvider would have access to the loading state.

Option 1

What you could do is to move wrap the entire app.js in LoadingProvider in the parent, maybe index.js.

In index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import './index.css';
import './bootstrap.min.css';
import App from './App';

ReactDOM.render(
    <LoadingProvider>
        <App />
    </LoadingProvider>,
    document.getElementById('root')
);

OR

Option 2

You could create a child component of everything inside of app.js.

In Child.js (Name it as you please)

const Child = ({ Component, pageProps }) => {
    const { loading } = useLoading();
     <AppStateProvider>
          { loading && <LoadingScreen loading={true} bgColor='#fff' spinnerColor={'#00A1FF'} textColor='#676767'></LoadingScreen> }
          <Layout>
            <Component {...pageProps} />
          </Layout>
     </AppStateProvider>
};

export default Child;

And in app.js

export default function App({ Component, pageProps }) {
  return (
    <>
      <CssBaseline />
      <LoadingProvider>
        <Child Component={Component} pageProps={pageProps}/>
      </LoadingProvider>
    </>
  );
};

OR

Option 3

You could create a loader component of the loader and import it into app.js.

In Loader.js (Name it as you please)

const Loader = () => {
    const { loading } = useLoading();
    return loading && <LoadingScreen loading={true} bgColor='#fff' spinnerColor={'#00A1FF'} textColor='#676767'></LoadingScreen>;        
};

export default Loader;

And in app.js

export default function App({ Component, pageProps }) {
  return (
    <>
      <CssBaseline />
      <LoadingProvider>
        <AppStateProvider>
          <Loader />
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </AppStateProvider>
      </LoadingProvider>
    </>
  );
};
G Sriram
  • 399
  • 1
  • 5
  • Thank you for your answer, in the second option that you've provided, where does the `...PageProps` is coming from? – ytpm Feb 13 '23 at 13:53
  • Btw, I cannot find the `ReactDOM.render` in my project. Does it has anything to do with that I am using `Next.js`? – ytpm Feb 13 '23 at 13:55
  • Pageprops needs to be passed from `app.js` to the `Child`. – G Sriram Feb 13 '23 at 13:56
  • Oh, just saw that you've made some changes to the answer. Trying it now. Thanks! – ytpm Feb 13 '23 at 13:58
  • `Next.js` probably does `ReactDOM.render` under the hood, you would probably not be able to see it. The idea is to delegate the `LoadingProvider` to the parent of `app.js` so that you can access the state inside. However if `app.js` is a seperate page since you are using Next.js, you could use the second approach of creating a child component. – G Sriram Feb 13 '23 at 13:58
  • Ok, i've made some changes according to your changes and implemented the second solution that you've provided, but I can only see white screen. – ytpm Feb 13 '23 at 14:02
  • I made a small error in the second option where I didn't add the return keyword. I also added an alternate solution, both should work, please give it a try! – G Sriram Feb 13 '23 at 14:08
  • The problem is what I had before, it adds the `LoadingScreen` before the `__next` `div` and not on top of it. – ytpm Feb 13 '23 at 14:27
  • Okay, that is because you are rendering the other divs later on. You could use option 2 and use the ternary operator to only render loading screen if it is loading. Or if you want to use option 3, you could make the loading screen come on top of the main screen using `position: absolute;` in CSS. I would recommend using the former. – G Sriram Feb 13 '23 at 14:29
  • I did it and it shows, but it shows in the middle of the main content and not on the top of `SideBar` and `AppBar`. (I used the 3rd solution option that you provided) – ytpm Feb 13 '23 at 14:35
  • I guess you would need to play around with css for it. I believe the context logic is working now! – G Sriram Feb 15 '23 at 05:09