51

I am trying to implement a loading screen when changing routes in my Next.js 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 ?

tanguy_k
  • 11,307
  • 6
  • 54
  • 58
Muljayan
  • 3,588
  • 10
  • 30
  • 54

6 Answers6

68

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
  • 815
  • 7
  • 8
  • 4
    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
  • 2
    It seems using `router.asPath` instead of `router.pathname` is more accurate if using dynamic routes. – Rui Ying Jul 01 '20 at 04:55
  • 4
    `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
  • @bubbleChaser it works fine for me. and also query pram changing is not detected. that's cool – Nilupul Heshan Nov 25 '21 at 05:17
  • 1
    I confirm behavior by @dhidy. on `routeChangeComplete` `router.asPath` has still the old pathvalue . Example starting from /a and clicking /b link, on routeChangeComplete router.asPath has url /a (NOT /b) – Kristi Jorgji Feb 03 '22 at 12:21
  • 2
    For some reason, this didn't work for me. The `loading` is always true after the first loading. – Akshay K Nair Jun 11 '22 at 13:36
  • Works perfectly thanks. Just need find a way to export the function to use for all my pages. – Tankiso Thebe Sep 23 '22 at 09:33
  • Doesn't work for me with query params. Skipping the equality check made it work. Is there any reason the equality check is added ? @RohitKrishnan – Karan Shah Apr 11 '23 at 06:37
  • @KaranShah yeah you need to compare the current path and the path that it is routing to.. if it is the same you don't need a loading state.. otherwise even clicking on the current page's link will trigger the loading state. You can skip the equality check if you don't mind this. – Rohit Krishnan Jun 08 '23 at 12:20
  • This is not working on App Router anymore. What is the workaround? – Renan Coelho Jul 20 '23 at 13:51
62

(Edit: If you're using the App Router, please see Jeremy Bernier's comment instead, below)

For anyone coming across this in 2023 (and using the old page router), the package nextjs-progressbar makes this super easy. In your Next.js _app.js (or .jsx, .tsx, etc.), simply add the ` anywhere in the default export, like below:

import NextNProgress from 'nextjs-progressbar';

export default function App({ Component, pageProps }) {
  return (
    <>
      <NextNProgress />
      <Component {...pageProps} />;
    </>
  );
}

And done!

Demo and screenshot: enter image description here

i-know-nothing
  • 789
  • 1
  • 7
  • 14
52

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
  • 325
  • 2
  • 3
  • 15
MoHo
  • 2,402
  • 1
  • 21
  • 22
  • 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
  • 3
    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. – Prav Aug 07 '20 at 14:51
  • 2
    For people using Next.js, there's this now: https://www.npmjs.com/package/nextjs-progressbar – FooBar May 19 '21 at 21:43
21

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
  • 918
  • 13
  • 18
4

Progress bar like NProgress in 90 lines of code (vs NProgress v0.2.0 is 470 lines .js + 70 lines .css):

import { useEffect, useReducer, useRef } from 'react';

import { assert } from './assert';
import { wait } from './wait';
import { getRandomInt } from './getRandomNumber';

let waitController: AbortController | undefined;

// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function useProgressBar({
  trickleMaxWidth = 94,
  trickleIncrementMin = 1,
  trickleIncrementMax = 5,
  dropMinSpeed = 50,
  dropMaxSpeed = 150,
  transitionSpeed = 600
} = {}) {
  // https://stackoverflow.com/a/66436476
  const [, forceUpdate] = useReducer(x => x + 1, 0);

  // https://github.com/facebook/react/issues/14010#issuecomment-433788147
  const widthRef = useRef(0);

  function setWidth(value: number) {
    widthRef.current = value;
    forceUpdate();
  }

  async function trickle() {
    if (widthRef.current < trickleMaxWidth) {
      const inc =
        widthRef.current +
        getRandomInt(trickleIncrementMin, trickleIncrementMax); // ~3
      setWidth(inc);
      try {
        await wait(getRandomInt(dropMinSpeed, dropMaxSpeed) /* ~100 ms */, {
          signal: waitController!.signal
        });
        await trickle();
      } catch {
        // Current loop aborted: a new route has been started
      }
    }
  }

  async function start() {
    // Abort current loops if any: a new route has been started
    waitController?.abort();
    waitController = new AbortController();

    // Force the show the JSX
    setWidth(1);
    await wait(0);

    await trickle();
  }

  async function complete() {
    assert(
      waitController !== undefined,
      'Make sure start() is called before calling complete()'
    );
    setWidth(100);
    try {
      await wait(transitionSpeed, { signal: waitController.signal });
      setWidth(0);
    } catch {
      // Current loop aborted: a new route has been started
    }
  }

  function reset() {
    // Abort current loops if any
    waitController?.abort();
    setWidth(0);
  }

  useEffect(() => {
    return () => {
      // Abort current loops if any
      waitController?.abort();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return {
    start,
    complete,
    reset,
    width: widthRef.current
  };
}
import { useRouter } from 'next/router';
import { useEffect } from 'react';

import { useProgressBar } from './useProgressBar';

const transitionSpeed = 600;

// https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047
export function RouterProgressBar(
  props?: Parameters<typeof useProgressBar>[0]
) {
  const { events } = useRouter();

  const { width, start, complete, reset } = useProgressBar({
    transitionSpeed,
    ...props
  });

  useEffect(() => {
    events.on('routeChangeStart', start);
    events.on('routeChangeComplete', complete);
    events.on('routeChangeError', reset); // Typical case: "Route Cancelled"

    return () => {
      events.off('routeChangeStart', start);
      events.off('routeChangeComplete', complete);
      events.off('routeChangeError', reset);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return width > 0 ? (
    // Use Bootstrap, Material UI, Tailwind CSS... to style the progress bar
    <div
      className="progress fixed-top bg-transparent rounded-0"
      style={{
        height: 3, // GitHub turbo-progress-bar height is 3px
        zIndex: 1091 // $zindex-toast + 1 => always visible
      }}
    >
      <div
        className="progress-bar"
        style={{
          width: `${width}%`,
          //transition: 'none',
          transition: `width ${width > 1 ? transitionSpeed : 0}ms ease`
        }}
      />
    </div>
  ) : null;
}

How to use:

// pages/_app.tsx

import { AppProps } from 'next/app';
import Head from 'next/head';

import { RouterProgressBar } from './RouterProgressBar';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <title>My title</title>
        <meta name="description" content="My description" />
      </Head>

      <RouterProgressBar />

      <Component {...pageProps} />
    </>
  );
}

More here: https://gist.github.com/tkrotoff/db8a8106cc93ae797ea968d78ea28047

tanguy_k
  • 11,307
  • 6
  • 54
  • 58
0

It is considerably easy by using nprogress

"use client"

import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

export default async function Home() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    NProgress.done();
    return () => {
      NProgress.start();
    };
  }, [pathname, searchParams]);

  return (
    <>
     ...
    </>
  )
}
krishnaacharyaa
  • 14,953
  • 4
  • 49
  • 88