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? :)
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();
}
};
}
Here is the solution
const LazyLoad = () => {
useEffect(() => {
NProgress.start();
return () => {
NProgress.stop();
};
});
return '';
};
<Suspense fallback={<LazyLoad />}>
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 "";
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With