8

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.

Maxrain
  • 221
  • 1
  • 4
  • 10

4 Answers4

19

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 />}
      ...
    );
}
Ben Stickley
  • 1,551
  • 1
  • 16
  • 22
  • Looks good. Thanks. Gonna try it out soon – Maxrain Sep 14 '21 at 08:22
  • 2
    ts variant of this? – kakabali Nov 26 '21 at 19:16
  • 2
    @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: https://kentcdodds.com/blog/how-to-use-react-context-effectively – Ben Stickley Nov 26 '21 at 21:36
  • @BenStickley Thanks a lot for the very exact link Upvoted – kakabali Nov 26 '21 at 23:12
  • 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). – Watchmaker Mar 03 '22 at 15:59
  • 1
    @Watchmaker, see https://kentcdodds.com/blog/application-state-management-with-react#what-about-performance – Ben Stickley Mar 03 '22 at 21:08
  • where is common loader component? do we need to import and use in every screeen? – Rajesh N Aug 04 '22 at 04:59
  • 1
    @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. – Ben Stickley Aug 04 '22 at 10:33
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;
ajay 24
  • 304
  • 3
  • 13
  • 1
    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/false – Maxrain Mar 14 '21 at 13:57
  • 1
    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. – ajay 24 Mar 15 '21 at 15:13
  • @ajay24, if you are posting a code, please post working code, not pseudo. Your source has alot dependencies, eg.: Styled, Spinner, Color, etc... – mst Oct 07 '22 at 23:15
2

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);
Rajesh N
  • 6,198
  • 2
  • 47
  • 58
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>
  );
};

RedClouds
  • 1
  • 3