Adding a Navigation Loader in Next.js 13+ with Custom Link and Router Wrappers

Home Blog Adding a Navigation Loader in Next.js 13+ with Custom Link and Router Wrappers

Adding a Navigation Loader in Next.js 13+ with Custom Link and Router Wrappers

Navigating between pages in a Nextjs application often leaves users wondering if anything is happening behind the scenes, especially if the route change takes a moment. Adding a navigation loader can significantly improve the user experience by providing visual feedback during these transitions. In this guide, we'll show you how to implement a navigation loader in Nextjs 13 and above using custom Link and Router wrappers.

Live Demo

Why the Change?

In Next.js 12, navigation events like routeChangeStart and routeChangeComplete were readily available for detecting route changes. However, these events were removed in Next js 13, prompting the need for alternative methods to detect navigation changes.

Tools and Libraries

We'll use the Jotai library for state management, but you're free to use any state handling library of your choice.

Step-by-Step Guide

1. Create a Custom Link Component

Let's start by creating a custom Link component to handle navigation state.

// custom-link.tsx
'use client'

import NextLink, { LinkProps } from 'next/link';
import { forwardRef } from 'react';
import { navigatingAtom } from '@/hooks/use-custom-router';
import { useAtom } from 'jotai';
import { usePathname } from 'next/navigation';

interface CustomLinkProps extends LinkProps {
    children: React.ReactNode;
    className?: string;
}

const Link = forwardRef<HTMLAnchorElement, CustomLinkProps>(
    (props, ref) => {
        const [navigating, setNavigating] = useAtom(navigatingAtom);
        const pathname = usePathname();

        const handleClick = () => {
            // Check if the path is changing
            if (props.href.toString().split('?')[0] !== pathname?.split('?')[0]) {
                setNavigating(true);
            }
        };

        return (
            <NextLink {...props} ref={ref} className={props.className} onClick={handleClick}>
                {props.children}
            </NextLink>
        );
    }
);

Link.displayName = 'Link';
export default Link;

In this component, we utilize usePathname to get the current path and compare it with the new path to determine if a navigation is occurring. When a navigation is detected, we set the navigating state to true.

2. Create a Custom Router Hook

Next, we need a custom Router hook to handle navigation state changes.

// custom-router.ts
import { atom, useAtom } from 'jotai';
import { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import { useRouter as useNextRouter, usePathname } from 'next/navigation';
import { useEffect } from 'react';

export const navigatingAtom = atom(false);

const useRouter = () => {
    const router = useNextRouter();
    const [navigating, setNavigating] = useAtom(navigatingAtom);
    const pathname = usePathname();

    useEffect(() => {
        const originalPush = router.push;

        router.push = async (href: string, options: NavigateOptions) => {
            // Check if the path is changing
            if (href.split('?')[0] !== pathname?.split('?')[0]) {
                setNavigating(true);
            }
            return originalPush(href, options);
        };

        // Cleanup on unmount
        return () => {
            router.push = originalPush;
        };
    }, [router, pathname]);

    return router;
};

export default useRouter;

Here, we override the router.push method to set the navigating state to true if the path is changing.

3. Create the Navigation Container Component

Finally, we create a component to display the loader during navigation.

// navigation.tsx
'use client';

import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { navigatingAtom } from '@/hooks/use-custom-router';
import { useAtom } from 'jotai';
import Image from 'next/image';

export function NavigationContainer() {
    const pathname = usePathname();
    const [navigating, setNavigating] = useAtom(navigatingAtom);

    useEffect(() => {
        // Reset navigating state when the pathname changes
        setNavigating(false);
    }, [pathname]);

    return (
        <>
            {navigating && (
                <div className='fixed w-screen h-screen bg-black bg-opacity-50 flex items-center justify-center z-[99999]'>
                    <div className='absolute w-32 h-32 animate-spin-reverse'>
                        <Image
                            src="/images/world.png"
                            alt="Loading"
                            fill
                            priority
                        />
                    </div>
                </div>
            )}
        </>
    );
}

This component listens to the navigating state and displays a loading spinner when navigation is in progress. We reset the navigating state once the pathname changes, indicating the navigation is complete.

Conclusion

By following these steps, you can seamlessly add a navigation loader to your Nextjs 13+ application. This ensures users have a visual indicator that something is happening during route changes, improving the overall user experience. Feel free to adjust the loader's design to match your application's aesthetics.

Additional Notes

  • We used Jotai for state management, but you can substitute it with any state management library you prefer.

  • Customize the loading spinner to fit your application's design requirements.

Implementing a navigation loader might seem complex at first, but with custom Link and Router wrappers, it becomes manageable and enhances the user experience significantly. Happy coding!

Support

Thank you for reading! If you enjoyed this post and want to support my work, consider supporting me by subscribing to my newsletter or sharing this post with a friend.