13

I am trying to implement a loading screen when changing routes in my Nextjs app ,for example /home -> /about.

My current implementation is as follows. I am setting the initial loaded state to false and then changing it on componentDidMount. I am also calling the Router.events.on function inside componentDidMount to change the loading state when the route change starts.

_app.js in pages folder

class MyApp extends App {
  constructor(props) {
    super(props);
    this.state = {
      loaded: false,
    };
  }

  componentDidMount() {
    this.setState({ loaded: true });
    Router.events.on('routeChangeStart', () => this.setState({ loaded: false }));
    Router.events.on('routeChangeComplete', () => this.setState({ loaded: true }));
  }



  render() {
    const { Component, pageProps } = this.props;

    const { loaded } = this.state;

    const visibleStyle = {
      display: '',
      transition: 'display 3s',
    };
    const inVisibleStyle = {
      display: 'none',
      transition: 'display 3s',
    };
    return (
      <Container>
        <>
          <span style={loaded ? inVisibleStyle : visibleStyle}>
            <Loader />
          </span>
          <span style={loaded ? visibleStyle : inVisibleStyle}>
            <Component {...pageProps} />
          </span>
        </>
      </Container>
    );
  }
}

This works perfectly fine but I feel like there may be a better solution more elegant solution. Is this the only way which isn't cumbersome to implement this loading feature or is there an alternative ?

Muljayan
  • 2,172
  • 4
  • 17
  • 43

3 Answers3

28

Why not use nprogress as follows in _app.js

import React from 'react';
import Router from 'next/router';
import App, { Container } from 'next/app';
import NProgress from 'nprogress';

NProgress.configure({ showSpinner: publicRuntimeConfig.NProgressShowSpinner });

Router.onRouteChangeStart = () => {
  // console.log('onRouteChangeStart triggered');
  NProgress.start();
};

Router.onRouteChangeComplete = () => {
  // console.log('onRouteChangeComplete triggered');
  NProgress.done();
};

Router.onRouteChangeError = () => {
  // console.log('onRouteChangeError triggered');
  NProgress.done();
};

export default class MyApp extends App { ... }

Link to nprogress.

You also need to include style file as well. If you put the css file in static directory, then you can access the style as follows:

<link rel="stylesheet" type="text/css" href="/static/css/nprogress.css" />

Make sure the CSS is available in all pages...

It will work for all your routes changing.

rdimaio
  • 327
  • 2
  • 3
  • 14
MoHo
  • 1,441
  • 15
  • 21
  • nprogress doesn't allow custom loaders right ? It only has that loading stripe on the top. – Muljayan Apr 15 '19 at 04:20
  • 1
    That's right. However you can change the nprogress to anything else you want. – MoHo Apr 15 '19 at 06:17
  • How do you change it ? I cant find it in the nprogress documentation. – Muljayan Apr 15 '19 at 10:22
  • 1
    what I meant was to use your custom component – MoHo Apr 16 '19 at 05:35
  • 2
    For someone reading this in the future, you can change the CSS by copying the styles from nprogress.css (inside node_modules) and pasting it in your own CSS/SCSS file and import your CSS file instead of nprogress.css file in your _app.js. You can fiddle with the CSS as animations are pretty basic. – Pravas Aug 07 '20 at 14:51
  • For people using Next.js, there's this now: https://www.npmjs.com/package/nextjs-progressbar – FooBar May 19 '21 at 21:43
25

Using the new hook api, this is how I would do it..

function Loading() {
    const router = useRouter();

    const [loading, setLoading] = useState(false);

    useEffect(() => {
        const handleStart = (url) => (url !== router.asPath) && setLoading(true);
        const handleComplete = (url) => (url === router.asPath) && setLoading(false);

        router.events.on('routeChangeStart', handleStart)
        router.events.on('routeChangeComplete', handleComplete)
        router.events.on('routeChangeError', handleComplete)

        return () => {
            router.events.off('routeChangeStart', handleStart)
            router.events.off('routeChangeComplete', handleComplete)
            router.events.off('routeChangeError', handleComplete)
        }
    })
    
    return loading && (<div>Loading....{/*I have an animation here*/}</div>);
}

Now <Loading/> is going to show up whenever the route will change... I animate this using react-spring, but you can use any library you prefer to do this.

You can even take a step further and modify when the component shows up by modifying the handleStart and handleComplete methods that gets a url.

Rohit Krishnan
  • 376
  • 4
  • 8
  • 1
    Hi, I would like to see how this is implemented in a full _app.js component. I will really appreciate if you can be so kind as to post a link to the file. – ArchNoob Dec 04 '19 at 14:49
  • 1
    It seems using `router.asPath` instead of `router.pathname` is more accurate if using dynamic routes. – Rui Ying Jul 01 '20 at 04:55
  • `router.asPath` in handleComplete doesn't seem to get resolved to the destination path in time. So I decided to not compare the url before `setLoading` and it works. – bubbleChaser Dec 26 '20 at 03:05
  • Hmm.. Removing the URL check isn't recommended because this can update the state even if there is no change in the URL. If you check the NextJS documentation, it mentions the router.asPath does not include the basePath and the locale. So please confirm whether you are using any those in your application so that I can update the code – Rohit Krishnan Mar 18 '21 at 18:24
  • in my case, I'm not compare on `routeChangeComplete` , I just set the state to false. because on `routeChangeComplete` the `router.asPath` value not change on first invoke, so I have to invoke it twice to change `router.asPath` value. – dhidy May 12 '21 at 19:33
  • That works too (you may want to handle `routeChangeError`). However this requires you to call this function in every page. How I used the example above is by adding it to the `_app.js`. In this case I would have to listen for the `routeChangeStart` too. – Rohit Krishnan May 18 '21 at 05:22
7

New Update with NProgress:

import Router from 'next/router'
import Link from 'next/link'
import Head from 'next/head'
import NProgress from 'nprogress'

Router.events.on('routeChangeStart', (url) => {
  console.log(`Loading: ${url}`)
  NProgress.start()
})
Router.events.on('routeChangeComplete', () => NProgress.done())
Router.events.on('routeChangeError', () => NProgress.done())

export default function App({ Component, pageProps }) {
  return (
    <>
      <Head>
        {/* Import CSS for nprogress */}
        <link rel="stylesheet" type="text/css" href="/nprogress.css" />
      </Head>
      <Component {...pageProps} />
    </>
  )
}

If you use Tailwind CSS, copy the code from here: https://unpkg.com/nprogress@0.2.0/nprogress.css and paste the code into your global CSS file.

if you want to disable the spinner add the below code in your _app.tsx/jsx file and remove the spinner styles from CSS.

NProgress.configure({ showSpinner: false });

Source Links:

Mejan
  • 422
  • 7
  • 13