Skip to main content

How To Handle Loading Between Page Changes in Next.js 14 App Router

00:02:41:39

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:

typescript
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:

typescript
'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

typescript
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.

typescript
import Link from 'next/link';
function Product() {
  return (
    <Link href="/about" passHref legacyBehavior>
      <BuyNowButton />
    </Link>
  );
}

export default Product;
typescript
"use client";

const BuyNowButton = React.forwardRef(({ onClick, href }, ref) => {
  return (
    <a href={href} onClick={onClick} ref={ref}>
      Buy now
    </a>
  )

Demo Link