Navigating from one page to another in a Next.js 14 app can sometimes be slow, especially in server heavy components. This can lead to a poor user experience as users may not know what is happening when they click on links. To address this issue, we can implement a loading indicator feature using Zustand for global state management. This guide will walk you through the process step by step.
First, let's set up Zustand to manage the loading state globally in our Next.js app. Create a file called common.ts
and add the following code:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type LoadingState = {
loading: boolean;
setLoading: (loading: boolean) => void;
};
export const useCommonStore = create;
Next, let's create a reusable loading component that displays an animated spinner while the application is loading. Create a new folder named components and inside it, create a file called Loader.tsx. Add the following code to display the loader:
'use client';
import { Player } from '@lottiefiles/react-lottie-player';
import animationData from './loader.json';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { useCommonStore } from '../../../store/common';
const Loader = () => {
const { loading, setLoading } = useCommonStore();
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const url = `${pathname}?${searchParams}`;
setLoading(false);
}, [pathname, searchParams, setLoading]);
return (
<>
{loading && (
<div>
<div className="fixed inset-0 z-40 filter blur-sm backdrop-filter backdrop-blur-sm"></div>
<div className="fixed inset-0 flex justify-center items-center z-50">
<div className="relative">
<Player
autoplay
loop
src={animationData}
style={{ height: '100px', width: '100px' }}
></Player>
</div>
</div>
</div>
)}
</>
);
};
export default Loader;
To ensure that the loader appears during navigation between pages in root layout.tsx file
import type { Metadata } from 'next';
import { GeistSans } from 'geist/font/sans';
import Providers from './Providers';
import { Navbar } from '../components/common/Navbar';
import { Footer } from '../components/common/Footer';
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Toaster } from 'sonner';
import { isMobileDevice } from '@/libs/responsive';
import { Session, getServerSession } from 'next-auth';
import { authOptions } from '@/libs/auth';
import { getTotalWishlist } from './(carts)/wishlist/action';
import '../styles/globals.css';
import Loader from '@/components/ui/loader';
import { Suspense } from 'react';
export const metadata: Metadata = {
title: 'The Happy Martz',
description: 'Buy online products at cheap price with sale prices',
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
let mobile = isMobileDevice();
const session: Session | null = await getServerSession(authOptions);
const totalItemsWishlists = await getTotalWishlist(session);
return (
<html lang="en">
<Providers>
<body className={GeistSans.className}>
<Suspense fallback={null}>
<Loader />
</Suspense>
<Navbar
session={session}
isMobile={mobile}
totalWishlists={totalItemsWishlists?.items.length}
/>
<main className="pointer-events-auto">
{children}
<Toaster position="top-right" />
<Analytics />
<SpeedInsights />
</main>
<Footer />
</body>
</Providers>
</html>
);
}
For showing the loader in server components, follow these steps:
Make a child client component of the button. Pass the reference of the Link component via React's forwardRef hook.
import Link from 'next/link';
function Product() {
return (
<Link href="/about" passHref legacyBehavior>
<BuyNowButton />
</Link>
);
}
export default Product;
"use client";
const BuyNowButton = React.forwardRef(({ onClick, href }, ref) => {
return (
<a href={href} onClick={onClick} ref={ref}>
Buy now
</a>
)