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.