Detect navigation event before it happens nextjs 13 app router, then prevent it from happening

2.8k Views Asked by At

With nextjs 12 (pages router) the router returned from 'next/router' had navigation events you could listen for and handle appropriately. Here is the documentation.

One specific event is routeChangeStart. When you want prompt a user before they leave a page you have listen for that event, then open a modal and throw an error to prevent navigation.

With nextjs 13 (app router) the router returned from 'next/navigation' does not have the same ability to subscribe to events.

There is a solution in the documentation to tell when a route has already changed. It uses the two new hooks

  • usePathname
  • useSearchParams

Here is that solution.

I want to know BEFORE a route is about to change.

How do you know when a nextjs 13 app (using the app router) is about to navigate, and how do you prevent it from doing so?

4

There are 4 best solutions below

1
Shahryar_Jahanshahloo On

If you are using the Link component for the naviation, you can switch it with a custom component and use the useRouter hook.

1
truongthi-bic On

To detect Navigate Event before it happens I use the hook usePathname + useRef + useEffect in the NavigationEvents (Link from Nextjs13)

My below code is checking if user navigate off /about

export function NavigationEvents() {

    // Get the current pathname from the router
    const pathname = usePathname()
    
    // Create a ref to store the previous pathname
    const ref = useRef(pathname)
    
    useEffect(() => {
    // Check if the pathname has changed and if the previous pathname was '/page'
    if (ref.current !== pathname &&
        ref.current === '/about') { 
        // Do something before the page changes
        // For example, you could show a confirmation dialog or save some data
    }
    
    // Update the ref to store the current pathname
    ref.current = pathname
    }, [pathname])
    
    // Return null because this component doesn't render any UI elements
    return null
}

Note: The NavigationEvents have to be add to Root layout for properly working (not child page layout)

reference photo

1
Glavin Sequeira On
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useRef } from "react";
import "../../css/loader.css";

export default function Loader() {
  const path = usePathname();
  const params = useSearchParams();
  const loaderRef = useRef(null);

  if (loaderRef?.current) {
    loaderRef.current.classList.remove("loading-bar-end");
    loaderRef.current.classList.add("loading-bar-start");
  }
  useEffect(() => {
    if (loaderRef?.current) {
      loaderRef.current.classList.remove("loading-bar-start");
      loaderRef.current.classList.add("loading-bar-end");
    }
  });

  return (
    <div className="fixed z-[50] top-0 w-full">
      <div
        ref={loaderRef}
        className="z-[50] bg-danger pb-1 rounded-full loading-bar-start"
      ></div>
    </div>
  );
}

Its actually very simple! I think their documentation makes it a little bit confusing but what is happening here is when you use Link or useRouter to navigate, the usePathname captures the path of the route without the route actually loading so you can perform like the start of the loading here and then in the useEffect you can stop the loading (using CSS animations or whatever you like). The dependencies for the useEffect can either be [path, params (useSearchParams*)] or nothing.Totally depends on you! Similary its the same for useSearchParams!

0
Matthew Westerham On

Credit goes to Steven Linn (github)

Note: This is only the solution to the FIRST part of the question. That is "How do you know when a nextjs 13 app (using the app router) is about to navigate?" I do not believe this solution can be used to prevent navigating away. Additionally, it does not work with the back and forward buttons. But a half solution is better than no solution so here I am.

I ran into this problem but for next 14.1.0. The way I was able to listen for a route change before it occurred was by referencing this github comment. They recommend using their nextjs13-router-events package.

But seeing I needed this for next 14.1.0, I took my own (similar) steps. In summary:

  • Copy paste the components' code from RouteChangeProvider, Link, and useRouteChange.

  • Change all import Link from "next/link"; to import Link from "path/to/custom/Link";

  • Wrap your app with the RouteChangeProvider

import { RouteChangeProvider } from './RouteChangeProvider';
...
return (
  <RouteChangeProvider>
    {children}
  </RouteChangeProvider>
)
  • Use the hook in the components where you need to listen for route changes:
import useRouteChange from './useRouteChange';
...
export default function Component(props: any) {
  ...
  useRouteChange({
    onRouteChangeStart: () => {
      console.log('Put code here to run BEFORE route changes');
    },
    onRouteChangeComplete: () => {
      console.log('onComplete 3');
    }
  });
  ...
}
  • (Additional step for next 14.1.0) In the Link.tsx component, move the const { onRouteChangeStart } = useRouteChangeContext(); line above the if (!useLink) return <a href={href} onClick={onClick} {...rest} />;
  if (!useLink) return <a href={href} onClick={onClick} {...rest} />;

  const { onRouteChangeStart } = useRouteChangeContext();

becomes

  const { onRouteChangeStart } = useRouteChangeContext();

  if (!useLink) return <a href={href} onClick={onClick} {...rest} />;

  • (Additional step for next 14.1.0) In the RouteChangeProvider.tsx, add onRouteChangeComplete to the dependency of the useEffect using it.
useEffect(() => onRouteChangeComplete(), [pathname, searchParams]);

becomes

useEffect(() => onRouteChangeComplete(),[pathname, searchParams, onRouteChangeComplete]);

P.S. The last two steps were a hotfix for an error and warning I received when creating the production build.