Implementing NProgress in the Next.js 13 App Router
Tags: nextjs, frontend, ui, javascript
Published: 2023-09-01 03:38:59

As developers, we strive to create applications that are not just functional but also intuitive and user-friendly. One of the critical aspects that often get overlooked is the user's need for feedback, especially during transitions or while waiting for content to load. This is where loading indicators come into play, serving as a crucial element in Progressive Web Apps (PWAs).

Imagine clicking a button or navigating to a different page in an app and receiving no indication that something is happening. It's akin to pushing a door with no sign of it budging—an experience that can be both confusing and frustrating. This lack of feedback can lead users to question whether the application is working correctly or even prompt them to leave the platform altogether.

Loading indicators, such as progress bars or spinners, provide that much-needed feedback. They inform the user that their request is being processed, offering assurance that the app is functioning as it should. Among the various libraries available for this purpose, NProgress stands out for its simplicity and ease of integration.

There is currently an NPM package called *Nextjs Progressbar* that nicely implements NProgress. However, this solution only works with the old `Pages` router and relies on `Next/Router`. The new `App` router that many (myself included) are beginning to adopt does not use this library. Router events are no longer available. This is due to Next.js 13's SSR-first design.

That being said, I've created a new NPM package called [nextjs13-progress](https://www.npmjs.com/package/nextjs13-progress) that works perfectly with the new App router!

Here's a rundown of how it works. The first thing is since we have no more router events from good ol' `useRouter`, we need to call `NProgress.start()` manually. I did this by creating a wrapper for `Next/Link` that calls `NProgress.start()` if we are linking to an internal route. It will automatically detect if the progress bar should be started via `shouldTriggerStartEvent.`

```jsx
import NextLink from "next/link";
import { forwardRef } from "react";
import NProgress from "nprogress";

import { shouldTriggerStartEvent } from "./should-trigger-start-event";

export const Link = forwardRef<HTMLAnchorElement, React.ComponentProps<"a">>(function Link(
  { href, onClick, ...rest },
  ref,
) {
  const useLink = href && href.startsWith("/");
  if (!useLink) return <a href={href} onClick={onClick} {...rest} />;

  return (
    <NextLink
      href={href}
      onClick={(event) => {
        if (shouldTriggerStartEvent(href, event)) NProgress.start();
        if (onClick) onClick(event);
      }}
      {...rest}
      ref={ref}
    />
  );
});
```

Now we can use this version of `Link` anywhere in our app that links to an App route.

Next, we can call `NProgress.done()` by creating a suspense boundary in our main `layout.tsx`:

```jsx
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation';
import NProgress from "nprogress";

function NProgressDone() {
	const pathname = usePathname();
	const searchParams = useSearchParams();
	useEffect(() => NProgress.done(), [pathname, searchParams]);
	return null;
}

return (
	<Suspense fallback={null}>
		<NProgressDone />
	</Suspense>
)
```

That's it! Now we have NProgress for the App router! 🎉

Also, if you're curious, here's the code for `shouldTriggerStartEvent`:

```ts
import { addBasePath } from "next/dist/client/add-base-path";

function getURL(href: string): URL {
  return new URL(addBasePath(href), location.href);
}

// https://github.com/vercel/next.js/blob/400ccf7b1c802c94127d8d8e0d5e9bdf9aab270c/packages/next/src/client/link.tsx#L169
function isModifiedEvent(event: React.MouseEvent): boolean {
  const eventTarget = event.currentTarget as HTMLAnchorElement | SVGAElement;
  const target = eventTarget.getAttribute("target");
  return (
    (target && target !== "_self") ||
    event.metaKey ||
    event.ctrlKey ||
    event.shiftKey ||
    event.altKey || // triggers resource download
    (event.nativeEvent && event.nativeEvent.button === 1)
  );
}

export function shouldTriggerStartEvent(href: string, clickEvent?: React.MouseEvent) {
  const current = window.location;
  const target = getURL(href);

  if (clickEvent && isModifiedEvent(clickEvent)) return false; // modified events: fallback to browser behaviour
  if (current.origin !== target.origin) return false; // external URL
  if (current.pathname === target.pathname && current.search === target.search) return false; // same URL

  return true;
}
```

We need this code to prevent NProgress from starting on things like external links and modified click events (alt, control, shift, etc).

---
## Thanks

Special thanks to [# Vũ Văn Dũng](https://github.com/joulev) for his [nextjs13-appdir-router-events](https://github.com/joulev/nextjs13-appdir-router-events) demo Next.js project. I borrowed much of the code from that project to make this package.

## Links

1. [GitHub Repo](https://github.com/designly1/nextjs13-progress/tree/9f3b603884d53ee2218291b441559ff5acb45393)
2. [Demo Site](https://nextjs13-progress-demo.vercel.app/)
3. [NPM Package](https://www.npmjs.com/package/nextjs13-progress)

Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the [Designly Blog](https://blog.designly.biz). Also, please leave your comments! I love to hear thoughts from my readers.

If you want to support me, please follow me on [Spotify](https://open.spotify.com/album/2fq9S51ULwPmRM6EdCJAaJ?si=USeZDsmYSKSaGpcrSJJsGg)!

Looking for a web developer? I'm available for hire! To inquire, please fill out a [contact form](https://designly.biz/contact).