Using Nprogress on React Router v7

Nprogress is a popular JS library to showing beautiful progress bar. I was using simple React + Vite without any framework. With react router dom, all the pages loads within a second on link click, even though internet connection is off. Because react already loaded those through js on initial load.

SEO is really hard with simple single page application. I noticed Google sometimes do not render JS, CSS assets when indexing. Because of that, no meta tags or title tags shown to Google bot.

I was looking for framework that is easy to set-up. Got to know Next JS is not beginner friendly.

After much research, I found Remix is now React Router v7. That was game changer for react lovers.

Now I can server side render full HTML just using React Router. I started building my react router apps and it’s same like Remix. But sometimes, it took 3-5 seconds to load when user clicks a link. I need a progress bar 🤔

I was already familiar with Nprogress. Finally, I set-up Nprogress for my react router app with the help of Ai. 💪

My project is in Typescript.

Here is what to do:

1. Install Nprogress

Double click to copy!

npm install nprogress
npm install --save-dev @types/nprogress

2. Add Nprogress Hook

// app/hooks/useNProgress.ts

import { useNavigation, useFetchers } from "react-router";
import { useEffect, useRef } from "react";
import NProgress from "nprogress";
import "nprogress/nprogress.css";

export function useNProgress(delay: number = 300) {
  const navigation = useNavigation();
  const fetchers = useFetchers();

  const timerRef = useRef<NodeJS.Timeout | null>(null);
  const startedRef = useRef(false);

  // Configure NProgress once per hook instance
  useEffect(() => {
    NProgress.configure({
      showSpinner: false,
      // You can add more options here like minimum, easing, speed, etc.
    });
  }, []);

  useEffect(() => {
    const fetchersIdle = fetchers.every((f) => f.state === "idle");

    if (navigation.state === "idle" && fetchersIdle) {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
      if (startedRef.current) {
        NProgress.done();
        startedRef.current = false;
      }
    } else {
      if (!timerRef.current) {
        timerRef.current = setTimeout(() => {
          NProgress.start();
          startedRef.current = true;
        }, delay);
      }
    }

    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [navigation.state, fetchers, delay]);

  // Also cleanup on unmount
  useEffect(() => {
    return () => {
      NProgress.done();
    };
  }, []);
}

3. Add Nprogress in the entry file

// app/root.tsx

import { useNProgress } from "@/hooks/useNProgress";

function AppProviders({ children }: { children: React.ReactNode }) {
  // Nprogress here
  useNProgress();

  return (
    <QueryClientProvider client={queryClient}>
      <TooltipProvider>
        <Toaster richColors />
        {children}
      </TooltipProvider>
    </QueryClientProvider>
  );
}

export default function App() {
  return (
    <AppProviders>
      <Outlet />
    </AppProviders>
  );
}

You can mention custom delay with:

useNProgress(500);

Otherwise simply use:

useNProgress();

The default 300ms delay ensure that Nprogress don’t appear for super fast navigation. Only start appearing if navigation take more than 300ms after link click.

4. Customize (Optional)

In your app.css

/* Customize progress bar */
  #nprogress .bar {
    background: var(--primary) !important;
    height: 3px !important;
  }
  #nprogress .peg {
    box-shadow: 0 0 10px var(--primary), 0 0 5px var(--primary) !important;
  }

That’s it. Now your users are ready for better navigation experience. 🥳