Showing progress for page transitions in Next.js
Tags: nextjs, webdev
Published: 2024-06-27 14:33:31

**Written by [Elijah Agbonze](https://blog.logrocket.com/author/kapeelkokane/)✏️**

You know that awkward moment when your web app is doing something in response to a user action, but the user is waiting with no idea that it’s doing something? That’s one of the reasons your users run to find alternatives — as well as one of the reasons why the importance of loading or progress indicators cannot be overstated. 

Progress indicators are instrumental in telling the user that a requested action is being processed. We often use generic loading indicators to indicate actions for which the user doesn’t have to concern themselves with the details. For example, "An account is being created for you" or "Your file is being deleted." 

Then there are progress indicators where you should give the user some idea of the progress. A common example is for file downloads, where you might see a message like "Your file is downloading" along with the download percentage. 

For these reasons and many more, progress indicators are preferred for effective page transitions in today’s world. 

Most React routers — like [Next.js](https://nextjs.org/docs/app/building-your-application/routing) and [Remix React Router](https://remix.run/docs/en/main/discussion/react-router) — perform routing between pages without causing a full page reload, unlike traditional anchor tags `<a>`. As a result, based on the kind of rendering you perform, you may end up with that awkward moment when the user has clicked a link to another page, but it seems like nothing is happening: ![Next Js Page Showing User Clicking Docs Link. Docs Page Does Not Load Immediately, And No Indicator Is Shown](https://blog.logrocket.com/wp-content/uploads/2024/06/Next-js-page-user-clicking-Docs-link-delayed-page-load-no-indicator.gif) 

Well, we know something is happening — the contents of the next page are being fetched and rendered — but the user has no clue. This is what we hope to solve in this article. 

By the end of this article, you’ll be able to build a fully functional progress indicator for Next.js page transitions, and your users won’t be tempted to run to other websites to get what they need.

## Why is there a delay during Next.js page transitions?

If you are new to Next.js or unfamiliar with the concepts of [server-side rendering for the Pages Router](https://blog.logrocket.com/implementing-ssr-next-js-dynamic-routing-prefetching/) and [React Server Components for the App Router](https://blog.logrocket.com/react-server-components-next-js-13/), you might wonder, "Why the delay?" SSR and RSCs both allow you to render your page from the server, and as such you can fetch the data for that page before it opens up. 

Maybe you’re used to using client-side data fetching to get the information needed for a page. The difference here is that the page renders without all the necessary information, so you’d have to add loading indicators to indicate that the data is being fetched. 

In the case of server-side data fetching, all the information is fetched first before the page is rendered. As a result, there’s a delay when a user clicks a link. 

We can [use `getServerSideProps`](https://blog.logrocket.com/data-fetching-next-js-getserversideprops-getstaticprops/) to make use of server-side data fetching on the Pages Router:

```javascript
export default function Page({ data }) {  /* Render data... */ }

export async function getServerSideProps() {
  const res = await fetch(`https://.../data`)  
  const data = await res.json();

  return {
    props: { data }
  }
}
```

Meanwhile, on the App Router, we can just write any simple `fetch` function in any of the pages or layouts:

```javascript
async function getData() {
  const res = await fetch(`https://.../data`)  
  const data = await res.json();

  return data;
}

export default function Page() {  
  const data = await getData()

  // render data
}
```

## Building a global progress bar in NextJs

The first step to implementing a progress indicator for page transitions on your Next.js application is to build a progress bar component and make it global. Create a new Next.js application from scratch:

```bash
npx create-next-app@latest
```

For most of this tutorial, we’ll stick with the App Router for the sake of this tutorial, as it’s the option Next.js recommends. But before the end of this article, I’ll explain how you can get it done for the Pages Router as well. 

Now we have our Next.js app installed, the next thing we should do is to create a `hooks` folder inside the `app` directory. In this `hooks` folder, we need a custom Hook file called `useProgress.js`.

### The `useProgress` Hook

The `useProgress` Hook will be responsible for creating and managing a progress value within a certain limit — e.g., `0` to `100`. Once you’ve created the file, go ahead and paste the code below:

```javascript
"use client";
import { useEffect, useState } from "react";

export const useProgress = () => {
  const [state, setState] = useState("initial"); // initial, in-progress, complete
  const [value, setValue] = useState(0);

  const start = () => {
    setState("in-progress");
  };

  useEffect(() => {
    let t = setInterval(
      () => {
        if (state === "in-progress") {
          if (value >= 60 && value < 80) {
            setValue(value + 2);
          } else if (value >= 80 && value < 95) {
            setValue(value + 0.5);
          } else if (value >= 95) {
            setValue(95);
          } else {
            setValue(value + 5);
          }
        } else if (state === "complete") {
          setValue(100);
          clearInterval(t);
        }
      },
      state === "in-progress" ? 600 : null
    );

    return () => clearInterval(t); // cleanup
  }, [state, value]);

  const done = () => {
    setState("complete");
  };

  const reset = () => {
    setValue(0)
    setState("initial");
  };

  useEffect(() => {
    let t;
    if (value === 100) {
      t = setTimeout(() => {
        reset();
      }, 300);
    }

    return () => clearTimeout(t); // cleanup
  }, [value]);

  return {
    state,
    value,
    start,
    done,
    reset,
  };
};
```

The code above will result in the following: ![Progress Counter With Value Shown Increasing From 0 To 95 While Speed Decreases Gradually As Counter Approaches 95](https://blog.logrocket.com/wp-content/uploads/2024/06/Progress-counter-value-increasing-0-95-speed-decreases-gradually.gif) The `useProgress` Hook is a form of a countdown — or in this case, a count-up that does not go beyond `95`. The first `useEffect` Hook does this count, and to optimize and organize the count, the Hook only does it when the `start` function has been triggered. 

Once the function is triggered and the count starts, the speed of the count from `0` to `95` reduces gradually. When the counter gets to `95`, it stays there. This is because we don’t ever want the `value` to go beyond `100`, so no matter how long loading a page might take, the `useProgress` Hook is optimized to go on within that certain limit. 

This count setup is all based on preferences. You can always adjust yours to fit what you want. 

There is another `useEffect` Hook that resets the progress value. The delay of `300ms` gives time for the progress bar — which we will see next — to count to `100` before being cut back to `0`. 

Also, notice that there is a cleanup for each `useEffect` Hook used. This is important because you don’t want the states being changed when the component has been unmounted. 

Now, as you may have deduced from the code above, the progress value is not based on the content being downloaded. Since we can’t get the size of the loading page’s contents to give a real progress value, we instead created a limited progress value whereby the value doesn’t go beyond `100`. 

Keep in mind that it is possible to give a real progress value — however, getting the size of the loading page’s contents is complicated and often not worth the effort. Since our count setup adequately communicates to users that their page is loading, we won’t implement anything more complex in this tutorial.

### The `ProgressBar` component

Now that we have the `useProgress` Hook set up, let’s go ahead and create a `components` folder. In this folder, create a `ProgressBar.js` file. We’ll use the `ProgressBar` component as a wrapper for the pages where we intend to have a progress indicator. 

In this case, we want the progress indicator for the whole app, which means we should wrap it around the `app/layout.js` file. This is because we want to use the properties of the `useProgress` Hook across any of the pages and components within the `app/layout.js` layout. 

You might wonder, aren’t we going to call the `useProgress` Hook for each page or component that requires it? No. That would cause inconsistency because the progress bars of each page would work independently of each other. 

So, assuming you trigger the `start` function on `page-1`, the `ProgressBar` — which has its `useProgress` Hook — would not have access to the current `value` of the `useProgress` Hook triggered on `page-1`: ![Graphic Explanation Showing Logic For Why You Should Not Call The Useprogress Hook For Each Page Or Component That Requires It](https://blog.logrocket.com/wp-content/uploads/2024/06/Graphic-explanation-why-not-call-useProgress-Hook-each-page-component-requires-it.png) Instead, what we want to do is call the `useProgress` Hook in a parent component (the layout now) and find a way of passing it to each child component that needs access to it: ![Graphic Explanation Showing Correct Logic For Calling The Useprogress Hook In A Parent Component And Passing It To Each Child That Needs Access](https://blog.logrocket.com/wp-content/uploads/2024/06/Graphic-explanation-correctly-calling-useProgress-Hook-passing-each-child-component-needs-access.png) The easiest way to do this is with [React Context](https://blog.logrocket.com/react-context-api-deep-dive-examples/). Once you’ve created the `ProgressBar` file, go ahead and paste the code below into it:

```javascript
"use client";
import React, { createContext } from "react";
import { useProgress } from "../hooks/useProgress";

export const ProgressBarContext = createContext(null);

const ProgressBar = ({ children }) => {
  const progress = useProgress();

  return (
    <ProgressBarContext.Provider value={progress}>
      {progress.state !== "initial" && (
      <div
        className="fixed top-0 z-50 h-1 bg-gradient-to-r from-blue-500 to-blue-300 duration-300 transition-all ease-in-out"
        style={{ width: `${progress.value}%` }}
      />
      )}
      {children}
    </ProgressBarContext.Provider>
  );
};
export default ProgressBar;
```

In the code above, we created a `ProgressBarContext` and set a value of `progress` to its provider. Whenever we need the properties of the `useProgress` Hook, we can just call the `useContext` Hook and pass in the `ProgressBarContext`. 

Now, let’s head to the `app/layout.js` file to wrap the `ProgressBar` component around it like so:

```javascript
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ProgressBar>{children}</ProgressBar>
      </body>
    </html>
  );
}
```

Save your files and run the app — everything should work just fine. 

Now that we’ve succeeded in making the progress bar global, there is one more thing we can do to avoid duplicates. Create a new file named `useProgressBar` under the `hooks` folder and paste this code:

```javascript
'use client'
import { useContext } from "react";
import { ProgressBarContext } from "../components/ProgressBar";

export const useProgressBar = () => {
  const progress = useContext(ProgressBarContext);

  if (progress === null) {
    throw new Error(
      "useProgressBar must be used within the ProgressBarProvider"
    );
  }

  return progress;
};
```

This Hook uses the `useContext` Hook and the `ProgressBarContext` we created earlier to give access to the `progress` value for any page or component that needs it. 

Let’s take a look at an example usage of this `useProgressBar` Hook. Create a new file called `ProgressLink` under the components folder and paste the code below in it:

```javascript
"use client";
import { useProgressBar } from "../hooks/useProgressBar";

const ProgressLink = ({ children, rest }) => {
  const progress = useProgressBar();

  return (
    <button onClick={() => progress.start()} {...rest}>
      {children}
    </button>
  );
};

export default ProgressLink;
```

It’s called `ProgressLink` because it’s going to replace the `Link` component for all of our page transitions. But before that, let’s test out what we’ve been doing so far to make sure everything works as expected. 

Head to `app/page.js` and replace any of the `a` tags to the `ProgressLink` component. Save the file and click the link you changed. You should have something like this: ![Next Js Page Showing User Clicking Docs Link, Triggering A Progress Bar While Next Page Loads](https://blog.logrocket.com/wp-content/uploads/2024/06/Next-js-page-new-progress-bar-displayed-show-transition-progress-user-clicks-Docs-link.gif)

### The `startTransition` API

The `startTransition` API is an API from React that lets you make updates without blocking the UI. It's designed to prioritize rendering updates related to user interactions, such as clicks or keyboard inputs, over other background updates, ensuring a smoother and more responsive UI. 

For our use case, we need it to perform updates to the router as well as to indicate to our `progress` object when the page has fully loaded. Thus, let’s update our `components/ProgressLink` component to the following:

```javascript
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { startTransition } from "react";
import { useProgressBar } from "../hooks/useProgressBar";

const ProgressLink = ({ href, children, ...rest }) => {
  const router = useRouter();
  const progress = useProgressBar();

  const navigateToDestination = (e) => {
    e.preventDefault();
    progress.start(); // show the indicator

    startTransition(() => {
      router.push(href);
      progress.done(); // only runs when the destination page is fully loaded
    });
  };

  return (
    <Link href="" onClick={navigateToDestination} {...rest}>
      {children}
    </Link>
  );
};

export default ProgressLink;
```

Let’s add some content to the app. Create a `posts` folder inside the `app` directory and in it a `page.js` file. Paste the code below:

```javascript
import Image from "next/image";
import React from "react";

const getPosts = async () => {
  const res = await fetch(
    "https://jsonplaceholder.typicode.com/posts?_limit=10"
  );

  if (res.ok) {
    return new Promise((resolve, reject) => {
      setTimeout(async () => {
        resolve(await res.json());
      }, 1000); // create a custom delay
    });
  } else {
    [];
  }
};

const page = async () => {
  const posts = await getPosts();

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="mb-32 z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
        <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
          Get started by editing&nbsp;
          <code className="font-mono font-bold">app/page.js</code>
        </p>
        <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
          <a
            className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
            href="/"
          >
            By{" "}
            <Image
              src="/vercel.svg"
              alt="Vercel Logo"
              className="dark:invert"
              width={100}
              height={24}
              priority
            />
          </a>
        </div>
      </div>
      <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
        {posts.map((post) => (
          <a
            href={`https://jsonplaceholder.typicode.com/posts/${post.id}`}
            className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
            target="_blank"
            rel="noopener noreferrer"
            key={post.id}
          >
            <h2 className={`mb-3 text-2xl font-semibold`}>
              {post.title}
              <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
                -&gt;
              </span>
            </h2>
            <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>{post.body}</p>
          </a>
        ))}
      </div>
    </main>
  );
};

export default page;
```

Now head back to the `app/page.js` file. Where you had the `ProgressLink` component, change the `href` to `/posts`. Now save and run it, and that’s it! You’ve created your first progress bar indicator in Next.js. Here is the [GitHub repo](https://github.com/Elijah-trillionz/nextjs-progress-indicator-apps-router) for what we’ve seen so far.

## Notes regarding Next.js page transitions with the Pages Router

The reason we had to create the `ProgressLink` was to be able to use the `startTransition` function for each transition. Meanwhile, the reason we needed the `startTransition` is because the Next.js App Router doesn’t have router events like its Pages Router does. 

If you’re working with the Pages Router, all you have to do is listen for the `routeChangeStart` and `routeChangeComplete` event to be able to start and finish the progress bar indicator. 

You also wouldn’t need to use React Context because everything would take place in a single `_app.js` file. This includes listening to the events and triggering the `useProgress` Hook. 

Here is a [GitHub repo](https://github.com/Elijah-trillionz/nextjs-progress-indicator-pages-router) for using progress indicators in the Pages Router. You can explore the code and check out how it differs from the tutorial we went through above.

## Other loading indicators

There are also some good packages that provide progress indicators for page transitions, like [Nextjs-toploader](https://www.npmjs.com/package/nextjs-toploader?activeTab=readme) and [nProgress](https://www.npmjs.com/package/nprogress). But there are some drawbacks to using these packages. 

For example, nProgress only provides a progress bar, and you’d still have to use `startTransition` in the App Router or router events in the Pages Router to trigger the start and end of the progress bar. 

Meanwhile, Nextjs-toploader makes use of nProgress and handles all progress indicators for page transitions. All you’d have to do is import it on your global layout and it would handle the rest. 

While this sounds good, it is possible your codebase cannot afford the luxury of installing a package that depends on others to provide a simple progress indicator for your app. In such cases, creating it yourself — as we’ve seen how to do in this article — would be your best option.

## Conclusion

Progress indicators provide a crucial function in your Next.js application that can’t be overemphasized. It’s important to create a smooth UX that doesn’t leave users wondering, "Is this link working?" or "Did I click it?". You can easily do that with a progress indicator. 

In this tutorial, we looked at creating a custom Hook that provides a value the progress bar we created, which it then uses to indicate the progress of the page transition. Also, we used the `startTransition` function to update the router and progress without blocking the UI performance. 

Lastly, we looked at a couple of different Next.js progress bar package options, when to use them, and drawbacks to consider. That brings this article to a close. Thanks for reading and happy hacking.

---

##[LogRocket](https://lp.logrocket.com/blg/nextjs-signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket](https://lp.logrocket.com/blg/nextjs-signup).

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/nextjs-signup)

[LogRocket](https://lp.logrocket.com/blg/nextjs-signup) is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — [start monitoring for free](https://lp.logrocket.com/blg/nextjs-signup).