Enhancing User Experience in Next.js: Adding a Seamless Loading Indicator

Enhancing User Experience in Next.js: Adding a Seamless Loading Indicator

posted 4 min read

When building web applications with Next.js, ensuring a smooth and engaging user experience is paramount. Imagine a user clicking on a link, eagerly anticipating the new content, but instead facing a few seconds of uncertainty while the page compiles in the background. This momentary pause can disrupt the user’s flow and create a perception of slowness. To tackle this, adding a visually appealing loading indicator to your Next.js app can bridge this gap, providing immediate feedback and enhancing the overall browsing experience.

This article will guide you through the simple steps to implement a loading indicator at the top of a Next.js page, ensuring your application feels faster and more responsive.

This article assumes you already have your Next.js app installed and you've created a few pages. If you haven't, you can easily do so using the Next.js docs here

Once you have that set, let's get right into writing code!

Tech Stack: Next.js & TailwindCSS

Step 1: The Problem and Its Solution

Problem: When a user clicks on a link, Next.js takes a few seconds to compile the page whose link was requested before displaying it.
Solution: Create a link click event handler that targets whenever a link is clicked anywhere on the page.

We'll be placing this event handler inside a useEffect hook

useEffect(() => {
    let timer = null;
    const handleLinkClick = (event) => {
      let target = event.target;

      // this loop accounts for cases where nested elements inside anchor tags are 
      // clicked.
      while (target && target.tagName !== "A") {
          target = target.parentElement;
      }

      if (target.tagName === "A" && target.href) {
        setShowLoader(true);

        timer = setTimeout(() => {
          setShowLoader(false);
        }, 5000);
      }
    };

    document.addEventListener("click", handleLinkClick);

    return () => {
      document.removeEventListener("click", handleLinkClick);
      clearTimeout(timer);
    }
  }, []);

The above code sets up an event listener to detect when a link is clicked in the document. When this happens, it changes the state variable showLoader to true and back to false after 5 seconds.

The state variable showLoader will be used to display and hide the loading indicator at the top (we'll write the code for this shortly, the loading indicator).

Putting the handler inside a useEffect hook ensures that the event listener and timeout are properly cleaned up when the component unmounts. It also guarantees that the event listener is added only once when the component mounts (by making the dependency array empty [])

Step 2: The Loading Indicator

Here's the code:

"use client";
// loadingIndicator.jsx

import { useEffect, useState } from "react";

const LoadingIndicator = ({ children }) => {
  const [showLoader, setShowLoader] = useState(false);

  // your useEffect to handle link click event goes here

  return (
    <div className="">
      {/* position as the first element on your page */}
      <div className="mb-5">
        {showLoader && (
          <div
            className="h-1 bg-green-500 rounded-r-md w-full"
          ></div>
        )}
      </div>

      <main>{children}</main>
    </div>
  );
};

export default LoadingIndicator;

The LoadingIndicator component will be a HOC (Higher Order Component). Why? So that the indicator animation will appear at the top of all pages it wraps.

Step 3: Wrap your Pages with the LoadingIndicator HOC

Basically like this:

// wrapping your product page with the loadingIndicator

const Product = () => {
  return (
    <LoadingIndicator>
      <div className="">
        {// content will be here}
      </div>
    </LoadingIndicator>
  );
};

export default Product;

Do the same for all other pages.

After doing this, you can click on any link on any page to see if there's a noticeable change. There should be if you've done everything right.

Notice the green line that stretches the entire width of the page at the top. Now let's animate it

Step 4: Animating the loading indicator

To animate the loading indicator, we'll need to increment its width at an interval (per second for example).

First, let's create a state variable that can be used as the width

const [percentage, setPercentage] = useState(0);

Then we use setInterval inside a separate useEffect hook to increment it

   let interval = null;
   useEffect(() => {
    if (showLoader) {
      interval = setInterval(() => {
        setPercentage((prevPercentage) => {
          const nextPercentage = prevPercentage + 1;
          return nextPercentage >= 100 ? 0 : nextPercentage;
        });
      }, 50);
    }

    return () => clearInterval(interval);
  }, [showLoader]);

We pass showLoader in the dependency array so the interval runs whenever the value for showLoader changes and specifically, we showLoader is true.

Finally, we use the percentage value as the value for width

{showLoader && (
          <div
            className="h-1 bg-green-500 rounded-r-md"
            style={{ width: `${percentage}%` }}
          ></div>
 )}

Step 5: The complete LoadingIndicator code

"use client";

import { useEffect, useState } from "react";

const LoadingIndicator = ({ children }) => {
  const [showLoader, setShowLoader] = useState(false);
  const [percentage, setPercentage] = useState(0);

  let interval = null;

  useEffect(() => {
    let timer = null;
    const handleLinkClick = (event) => {
      const target = event.target;

      // this loop accounts for cases where nested elements inside anchor tags are 
      // clicked.
      while (target && target.tagName !== "A") {
          target = target.parentElement;
      }

      if (target.tagName === "A" && target.href) {
        setShowLoader(true);

        timer = setTimeout(() => {
          setShowLoader(false);
          clearInterval(interval); // to ensure animation stops
        }, 5000);
      }
    };

    document.addEventListener("click", handleLinkClick);

    return () => {
      document.removeEventListener("click", handleLinkClick);
      clearTimeout(timer);
    };
  }, []);

  useEffect(() => {

    if (showLoader) {
      interval = setInterval(() => {
        setPercentage((prevPercentage) => {
          const nextPercentage = prevPercentage + 1;
          return nextPercentage >= 100 ? 0 : nextPercentage;
        });
      }, 50);
    }

    return () => clearInterval(interval);
  }, [showLoader]);

  return (
    <div className="">
      {/* position as the first element on your page */}
      <div className="mb-5">
        {showLoader && (
          <div
            className="h-1 bg-green-500 rounded-r-md"
            style={{ width: `${percentage}%` }}
          ></div>
        )}
      </div>

      <main>{children}</main>
    </div>
  );
};

export default LoadingIndicator;


The gif above is what the final result should look like

Conclusion

Adding a loading indicator to your Next.js application significantly enhances the user experience, making navigation feel faster and more intuitive. Leveraging LoadingIndicator lets you provide immediate visual feedback during page transitions, minimizing uncertainty and improving user engagement. This small yet impactful addition bridges the gap during compilation and contributes to a smoother, more polished browsing experience. Customize the indicator to align with your application's design, and enjoy the benefits of a more responsive and user-friendly interface.

If you read this far, tweet to the author to show them you care. Tweet a Thanks

More Posts

A Web App to Showcase all your GitHub Projects

2KAbhishek - May 6

Mastering React: A Mindset for Component-Centric Development

Losalini Rokocakau - May 3

Getting familiar with Readable and Writable streams in Node.js

Eda - Sep 22

Rate limiting middleware in ASP.NET Core using .NET 8.0

Hussein Mahdi - May 11
chevron_left