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? :)
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? :)
Share Improve this question edited Mar 20, 2020 at 3:58 Luiz Felipe asked Jun 18, 2019 at 2:08 Luiz FelipeLuiz Felipe 9971 gold badge13 silver badges23 bronze badges 4- I am trying to do the same thing did you ever find an answer to this question – Steve K Commented Aug 8, 2019 at 3:09
- No, I didn’t. :( – Luiz Felipe Commented Aug 8, 2019 at 10:14
- @LuizFelipe What about now? I was looking for the same thing. – Kutsan Kaplan Commented Feb 4, 2021 at 20:46
- 1 @KutsanKaplan, I left this idea behind. But maybe when the new concurrent mode is released we may be able to do something like this. – Luiz Felipe Commented Feb 4, 2021 at 21:14
4 Answers
Reset to default 6Here 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 "";
}
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 my solution using react hooks.
import React, { useEffect } from 'react';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const Loading = () => {
useEffect(() => {
NProgress.start();
return () => {
NProgress.done();
};
}, []);
return (
<Row>
<Col span={12} offset={6}>
Loading
</Col>
</Row>
);
};
export default Loading;
as you can see i used useEffect
to detect component status.
NProgress.start();
is called on component mountNProgress.done();
is called on component dismount as cleanup.
the return value is optional and you can render whatever you want.
you can also use class based components to achieve the same results. to do that you can use componentWillUnmount()
and componentDidMount()
.