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