Adding a cross-page loader in NextJS

To display a loading bar while loading the next page, we will use the NextJS hook useRouter which will provide us with a series of event listeners that we can subscribe to and take actions in response to a route change.

To display the loading bar, we will use NProgress:

npm install nprogress

The next thing we will do is to create a custom hook that we will call useLoading. Inside the hook, we will subscribe to the router events. When the path changes, we will run NProgress.start() to show the loading bar and when the path has finished loading, we will run NProgress.done() to hide it.

useLayout.tsx

import * as React from 'react'
import { useRouter } from 'next/router'
import NProgress from 'nprogress'

export const useLoading = () => {
  const router = useRouter()

  React.useEffect(() => {
    const handleStart = () => NProgress.start()
    const handleComplete = () => NProgress.done()

    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)
    }
  })
}

To display the loading bar on all pages, we need to add the NProgress styles globally. Remember to import that .css file into _app.tsx:

/* NProgress */

/* Make clicks pass-through */
#nprogress {
  pointer-events: none;
}

#nprogress .bar {
  background: #29d;

  position: fixed;
  z-index: 1031;
  top: 0;
  left: 0;

  width: 100%;
  height: 2px;
}

/* Fancy blur effect */
#nprogress .peg {
  display: block;
  position: absolute;
  right: 0px;
  width: 100px;
  height: 100%;
  box-shadow: 0 0 10px #29d, 0 0 5px #29d;
  opacity: 1;

  -webkit-transform: rotate(3deg) translate(0px, -4px);
  -ms-transform: rotate(3deg) translate(0px, -4px);
  transform: rotate(3deg) translate(0px, -4px);
}

/* Remove these to get rid of the spinner */
#nprogress .spinner {
  display: block;
  position: fixed;
  z-index: 1031;
  top: 15px;
  right: 15px;
}

#nprogress .spinner-icon {
  width: 18px;
  height: 18px;
  box-sizing: border-box;

  border: solid 2px transparent;
  border-top-color: #29d;
  border-left-color: #29d;
  border-radius: 50%;

  -webkit-animation: nprogress-spinner 400ms linear infinite;
  animation: nprogress-spinner 400ms linear infinite;
}

.nprogress-custom-parent {
  overflow: hidden;
  position: relative;
}

.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
  position: absolute;
}

@-webkit-keyframes nprogress-spinner {
  0% {
    -webkit-transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
  }
}
@keyframes nprogress-spinner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

Finally, we will import our custom hook in each page. A good idea is to create a Layout component that imports the hook and receives the content of each page as children. This way we reuse it in a more comfortable way.

import Head from 'next/head'
import { useLoading } from 'components/useLoading'

type LayoutProps = {
  children: React.ReactChild | React.ReactChild[]
  title: string
}

export const Layout: React.SFC<LayoutProps> = ({ children, title }) => {
  useLoading()
  return (
    <>
      <Head>
        <title>{title}</title>
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1"
        ></meta>
      </Head>
      {children}
    </>
  )
}