Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use NProgress with "React.lazy"

I have the following component tree:

<BrowserRouter>
  <Suspense fallback={<h1>MyFallback</h1>}>
    <Switch>
      <Route component={HomePage} path="/" exact />
      <Route
        component={lazy(() => import('./pages/Auth/Login'))}
        path="/auth/login"
        exact
      />
    </Switch>
  </Suspense>
</BrowserRouter>

I was using React.Suspense to show a loading fallback. However, now I want to show a progress bar at the top of the current page instead of using a normal Suspense loading fallback, which removes the entire current route to display the fallback.

How do I add NProgress, for example, to indicate the loading progress of the page that is being loaded?

Maybe the new React's Concurrent Mode can help with that? :)

like image 295
Luiz Felipe Avatar asked Jun 18 '19 02:06

Luiz Felipe


Video Answer


3 Answers

The below is not tested as I've pulled this out from a more advanced configuration, however it should work. If you have difficulty please post so that we can update and work through the issue thx.

npm install react-use react-helmet-async nprogress

Create hook called "useMounted"

import {useEffect, useRef} from 'react';
import {useUpdate} from 'react-use';

export default function useMounted() {
  const mounted = useRef(false);
  const update = useUpdate();
  useEffect(() => {
    if (mounted.current === false) {
      mounted.current = true;
      update();
    }
  }, [update]);
  return mounted.current;
}

Create "ProgressBar" component

This will allow you to pass props to customize your progress bar. Note this is a limited example, see NProgress css file for additional css styles you may wish to modify.

import {Helmet} from 'react-helmet-async';
import useMounted from '../hooks/useMounted'; // your path may differ.
import { useLocation } from 'react-router-dom'; // not needed for nextjs
import nprogress from 'nprogress';

const ProgressBar = (props?) => {
  
  props = {
    color: 'red',
    height: '2px',
    spinner: '20px',
    ...props
  };

  // if using NextJS you will not need the below "useMounted" hook
  // nor will you need the below "useEffect" both will be 
  // handled by the Router events in the below Bonus
  // monkey patch.
  
  const mounted = useMounted();
  const { pathname } = useLocation(); // assumes react router v6
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    if (!visible) {
      nprogress.start();
      setVisible(true);
    }
    if (visible) {
      nprogress.done();
      setVisible(false);
    }
    if (!visible && mounted) {
      setVisible(false);
      nprogress.done();
    }
    return () => {
      nprogress.done();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [pathname, mounted]);

  // if using the below styles with NextJS wrap the below in
  //     <style jsx global>{`styles here `}</style>;
  // you will not need Helmet either simply return the
  // jsx style.

  const styles = `
     #nprogress .bar {
        background: ${props.color};
        height: ${props.height};
     }
     #nprogress .peg {
        box-shadow: 0 0 10px ${props.color}, 0 0 5px ${props.color};
     }
     #nprogress .spinner-icon {
        width: ${props.spinner};
        height: ${props.spinner};
        border-top-color: ${props.color};
        border-left-color: ${props.color};
     }
  `;

  return (
    <Helmet>
      <style>{styles}</style>
    </Helmet>
  );
};
export default ProgressBar;

Use your Progress Bar Component

Shown here with default create-react-app application.

NOTE: This example is based on react-router version 6

import React from 'react';
import ReactDOM from 'react-dom';
import ProgressBar from './components/ProgressBar'; // your path may differ

import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter, Routes } from 'react-router-dom';

import './index.css';
import 'nprogress/nprogress.css';

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <ProgressBar />
      <Routes>
       {/* your routes here */}
      </Routes>
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

Bonus! Monkey patch fetch to trigger progress bar on fetches.

import nprogress from 'nprogress';
// import Router from 'next/router'; // uncomment for NextJS

function DOMEnabled() {
  return !!(
    typeof window !== 'undefined' &&
    window.document &&
    window.document.createElement
  );
}

// let timer: NodeJS.Timeout; // for typescript use.
let timer;
let state: string;
let activeRequests = 0;

const delay = 250;

function load() {
  if (state === 'loading') return;
  state = 'loading';
  timer = setTimeout(function () {
    nprogress.start();
  }, delay); // only show if longer than the delay
}

function stop() {
  if (activeRequests > 0) return;
  state = 'stop';
  clearTimeout(timer);
  nprogress.done();
}

// Uncomment if using [NextJS][2]

// Router.events.on('routeChangeStart', load);
// Router.events.on('routeChangeComplete', stop);
// Router.events.on('routeChangeError', stop);

if (DOMEnabled()) {
  const _fetch = window.fetch;
  window.fetch = async function (...args) {
    if (activeRequests === 0) load();
    activeRequests++;
    try {
      const result = await _fetch(...args);
      return result;
    } catch (ex) {
      return Promise.reject(ex);
    } finally {
      activeRequests -= 1;
      if (activeRequests === 0) stop();
    }
  };
}

like image 159
Blujedis Avatar answered Oct 26 '22 21:10

Blujedis


Here is the solution

const LazyLoad = () => {
    useEffect(() => {
        NProgress.start();

        return () => {
            NProgress.stop();
        };
    });

    return '';
};

<Suspense fallback={<LazyLoad />}>
like image 27
Tung Luong Thanh Avatar answered Oct 26 '22 19:10

Tung Luong Thanh


import { useEffect } from "react";
import NProgress from "nprogress";
import "nprogress/nprogress.css";

export default function TopProgressBar() {
  useEffect(() => {
    NProgress.configure({ showSpinner: false });
    NProgress.start();

    return () => {
      NProgress.done();
    };
  });

  return "";
}
like image 38
Rahul Mahajan Avatar answered Oct 26 '22 21:10

Rahul Mahajan