Global screen loader in react
Asked Answered
C

4

11

I am looking for a solution for using a global screen loader in react.

I am not that much familiar to react context, but I was wondering if that could help me here. Basically I am introducing a screenloader and I was thinking that maybe the best way would be to have a global loader somewhere in main component.So to conclude:

  • I want to have global loader in main component
  • I want to update the state of global loader wherever I want in app
  • I don't want to pollute all the components with ScreenLoaders where I need to use it
  • I want to use hooks for it

So is there a way to have a global state of loader/loaderText and setting and resetting whenever needed using context?

If there is a simple way to do it, then do you think there might be any drawbacks of using such solution? Maybe that's an overkill for it.

Chatham answered 12/3, 2021 at 0:40 Comment(0)
M
23

What about creating a custom hook, useLoading, which can be used in any component that gives access to loading and setLoading from global context?

// LoadingContext.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.jsx
import { LoadingProvider } from "./LoadingContext";

function App() {
    return (
        <LoadingProvider>
            <RestOfYourApp />
       </LoadingProvider>
    );
}
// RestOfYourApp.js
import { useLoading } from "./LoadingContext";

function RestOfYourApp() {
    const { loading, setLoading } = useLoading();
    return (
      {loading && <LoadingComponent />}
      ...
    );
}
Madalene answered 8/9, 2021 at 23:28 Comment(10)
Looks good. Thanks. Gonna try it out soonChatham
ts variant of this?Tuchun
@kakabali, my answer was partially inspired by this article which has some TS code examples. It's not exactly this, but it'll point you in the right direction: kentcdodds.com/blog/how-to-use-react-context-effectivelyMadalene
@BenStickley Thanks a lot for the very exact link UpvotedTuchun
I always wonder if having a global loading state has good performance considering that all connected components will receive an update when the loading state changes and probably there are many connected components (either with Redux or Context API).Falchion
@Watchmaker, see kentcdodds.com/blog/…Madalene
where is common loader component? do we need to import and use in every screeen?Jobina
@RajeshNasit, you don't need to import a common loader component in every screen. You import the hook, useLoading and then you can call setLoading(true) to show the loader. I've update above to be more clear.Madalene
This is a little late, but the problem here is that this assumes 1 api call. If you have multiple async calls, one of their "false" settings could invalidate the loading of numrous other "in-flight" api calls.Mcalister
@jamesemanon, which line of code above assumes 1 api call?Madalene
J
4

Some more easy way Create Provider with context and hook within single file

import React, {useRef} from 'react';
import {Loader} from '@components';

const LoaderContext = React.createContext();

export function LoaderProvider({children}) {
  const ref = useRef();
  const startLoader = () => ref.current.start();
  const stopLoader = () => ref.current.stop();
  const value = React.useMemo(
    () => ({ref, startLoader, stopLoader}),
    [ref, startLoader, stopLoader]
  );

  return (
    <LoaderContext.Provider value={value}>
      {children}
      <Loader ref={ref} />
    </LoaderContext.Provider>
  );
}

export const useLoader = () => React.useContext(LoaderContext);

in App.js add provider

import {StoreProvider} from 'easy-peasy';
import React from 'react';
import {StatusBar, View} from 'react-native';
import colors from './src/assets/colors';
import Navigation from './src/navigation/routes';
import {LoaderProvider} from './src/providers/LoaderProvider';
import {ToastProvider} from './src/providers/ToastProvider';
import store from './src/redux/store';
import globalStyles from './src/styles/index';

import('./src/helpers/ReactotronConfig');

function App() {
  return (
    <StoreProvider store={store}>
      <StatusBar
        barStyle="light-content"
        backgroundColor={colors.backgroundDark}
        translucent={false}
      />
      <ToastProvider>
        <LoaderProvider>
          <View style={globalStyles.flex}>
            <Navigation />
          </View>
        </LoaderProvider>
      </ToastProvider>
    </StoreProvider>
  );
}

export default App;

And in any screen use like this way

import {useLoader} from '../../providers/LoaderProvider';
const {startLoader, stopLoader} = useLoader();

Loader.js

import React, {forwardRef, useImperativeHandle, useState} from 'react';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
import {wp} from '../../styles/responsive';

function Loader(props, ref) {
  const [loading, setLoading] = useState(0);

  useImperativeHandle(
    ref,
    () => ({
      start: () => {
        const loadingCount = loading + 1;
        setLoading(loadingCount);
      },
      stop: () => {
        const loadingCount = loading > 0 ? loading - 1 : 0;
        setLoading(loadingCount);
      },
      isLoading: () => loading >= 1,
    }),
    [],
  );

  if (!loading) {
    return null;
  }

  return (
    <View style={styles.container}>
      <ActivityIndicator size={'small'} color={'#f0f'} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFill,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#11111150',
    zIndex: 999,
    elevation: 999,
  },
});

export default forwardRef(Loader);
Jobina answered 4/8, 2022 at 8:23 Comment(0)
H
2

useLoader.js (hook)

import React, { useState } from "react";
import Loader from "./loader";
const useLoader = () => {
  const [loading, setLoading] = useState(false);
  return [
    loading ? <Loader /> : null,
    () => setLoading(true),
    () => setLoading(false),
  ];
};
export default useLoader;

loader.js (loader componenet)

import React from "react";
import styled from "styled-components";
import spinner from "./loader.gif"; // create gif from https://loading.io
import Color from "../../Constant/Color";

const Loader = () => {
  return (
    <LoaderContainer>
      <LoaderImg src={spinner} />
    </LoaderContainer>
  );
};


const LoaderContainer = styled.div`
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  position: fixed;
  background: ${Color.greyBg};
  z-index: 100;
`;
const LoaderImg = styled.img`
  position: absolute;
`;

export default Loader;

Using Loader hook

import useLoader from "../../../hooks/loader/useLoader"; /// import loader hook

const App = (props) => {
const [loader, showLoader, hideLoader] = useLoader(); //initialize useLoader hook


useEffect(() => {
    showLoader(); /// loading starts
   
    Axios.post("url")
      .then((res) => {
        hideLoader(); // loading stops
      })
      .catch((error) => {
        hideLoader();// loading stops
      });
  }, []);

return (
<>
{loader} /// important 

//// add your elements /////
</>
)
}
export default App;
Hellgrammite answered 12/3, 2021 at 13:53 Comment(3)
But in this case I will always have to apply the {loader} to every component where I need it right? That's what I want to avoid. To have just one global loader html element in e.g App component and depending on some fetching within others component - set the loading state to true/falseChatham
For that you have to use redux. call this useLoader hook on the top level of your app e.g where you have set up the routing. And render {loder}. create one redux state of boolen value , add use effect hook in Awhere you have use the routing ,whenever you want to showloader() change the value of redux state to true and for hideloader set to false. If you don't want to use redux use ,useContext hook.Hellgrammite
@ajay24, if you are posting a code, please post working code, not pseudo. Your source has alot dependencies, eg.: Styled, Spinner, Color, etc...Amylose
S
0

You can use this package for simple react loading : https://www.npmjs.com/package/react-global-loading

Usage :

import { GlobalLoading, showLoading } from 'react-hot-toast';

const App = () => {
  const show = () => {
    showLoading(true);
    setTimeout(() => {
      showLoading(false);
    }, 1000);
  };

  return (
    <div>
      <button onClick={show}>Show Loading</button>
      <GlobalLoading />
    </div>
  );
};

Sow answered 8/2, 2023 at 9:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.