DEV Community

loading...

Use NProgress with Next.js (Router and fetch events)

Vincent Voyer
Building 🏗 https://turnshift.app, Shift Scheduling for Slack Teams. Also: https://nextjsnews.com https://github.com/vvo https://dev.to/vvo Before: JavaScript and support @algolia .
・2 min read

Today I was trying to add NProgress https://github.com/rstacruz/nprogress to my Next.js project.

I wanted the progress bar to:

  1. show when switching routes / pages
  2. show when any fetch call is made
  3. display only after a delay, I don't want to show a loader at EVERY interaction, only when the requests are "slow"

Here's a demo of how NProgress looks like:
Demo of NProgress

Since I hit met some challenges while implementing all of that, I felt like it would be good to share how I did it. So here it is:

First, install the nprogress package:

npm install nprogress
Enter fullscreen mode Exit fullscreen mode

Then edit or create your _app.js and add:

// global styles are required to be added to `_app.js` per Next.js requirements.
import "nprogress/nprogress.css";

const TopProgressBar = dynamic(
  () => {
    return import("components/TopProgressBar");
  },
  { ssr: false },
);

export default function MyApp({ Component, pageProps }) {
  return <>
    <TopProgressBar />
    <Component {...pageProps} />
  </>
}
Enter fullscreen mode Exit fullscreen mode

Here we use dynamic imports and the ssr option to make sure our TopProgressBar is loaded only in browser environements.

If you're wondering how relatively loading components/TopProgressBar works, just configure you jsconfig.json as shown in the Next.js documentation.

Finally create components/TopProgressBar.js:

import Router from "next/router";
import NProgress from "nprogress";

let timer;
let state;
let activeRequests = 0;
const delay = 250;

function load() {
  if (state === "loading") {
    return;
  }

  state = "loading";

  timer = setTimeout(function () {
    NProgress.start();
  }, delay); // only show progress bar if it takes longer than the delay
}

function stop() {
  if (activeRequests > 0) {
    return;
  }

  state = "stop";

  clearTimeout(timer);
  NProgress.done();
}

Router.events.on("routeChangeStart", load);
Router.events.on("routeChangeComplete", stop);
Router.events.on("routeChangeError", stop);

const originalFetch = window.fetch;
window.fetch = async function (...args) {
  if (activeRequests === 0) {
    load();
  }

  activeRequests++;

  try {
    const response = await originalFetch(...args);
    return response;
  } catch (error) {
    return Promise.reject(error);
  } finally {
    activeRequests -= 1;
    if (activeRequests === 0) {
      stop();
    }
  }
};

export default function () {
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Here we register to Next.js router events and monkey patch the global fetch. I was worried monkey patching fetch would fail to register early on but turns out it works 🤷‍♂️!

As you can see, TopProgressBar renders nothing, I guess there might be issues of doing things this way so if you encounter some, just let me know!

That's it!

If you're wondering if NProgress is still maintained because of the low number of commits and "high" number of issues, the good news is that they are working on a new version for 2020: https://github.com/rstacruz/nprogress/pull/218

Even if you're not using Next.js, you should be able to adapt this code to your favorite platform or framework.

Discussion (8)

Collapse
mxmzb profile image
Maxim

Nice, writeup. I instantly was reminded of doing this with Turbolinks in Rails back in the days.

Although it's technically a nice solution, I actually dislike the top progress bar on websites. Somehow, I always feel like I have to wait longer because of the progress bar. Maybe it's because only slow loading sites use it, and I am wrongly attributing it to the bar. 🤷‍♂️

Collapse
vvo profile image
Vincent Voyer Author

Yep indeed it can feel this way, when it's fast enough though and delayed then it's a good indicator that "the UI is up to date now"

Collapse
supermin profile image
Anturin Durimov • Edited

Hi, I like the solution. But if you use SWR with fetch, then with each revalidation the progress bar will appear. Is there a way to turn it off for SWR revalidate fetches?

Collapse
vvo profile image
Vincent Voyer Author • Edited

Good question; I am experiencing the same as you because of using SWR. For now, I am fine with it, but you're right; there should be a way to disable it.

Ways to do this:

  • use a custom fetcher that calls window. fetch() with some specific (non-standard) params allowing you to identify the calls to fetch are from SWR
  • in your fetch monkey patch, find the URL path, and if it's "/api" then you could not show the progress

If you try one of those or find other ways, let me know!

Collapse
antchinchilla profile image
Anthony Ch

Hi, it works good with routing but it doesnt work with fetch, im using node-fetch, is this the issue? which fetch package are you using for this? thanks!

Collapse
vvo profile image
Vincent Voyer Author

Hi there, with the latest Next.js versions you do not even have to include any fetch polyfill, so you can remove any fetch package and it should just works

Collapse
titungdup profile image
dhondup

Hi, Does NProgress also work for initial site load? Like a loader?

Collapse
titungdup profile image
dhondup

Also, for some reason, the code is not working after adding TopProgressBar in _app.js