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

4

13

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?

Yowl answered 13/10, 2023 at 14:23 Comment(1)
This might not be possible, many are asking: #76981349 #77296806 #76774384 and no answer yet replicates previous functionality. I also found nothing in the github issues. This is pretty common functionality, I'm genuinely surprised I haven't found an out of the box solution yet.Cyn
G
0

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

Gut answered 22/10, 2023 at 10:13 Comment(1)
Thanks for your response @Shahryar_Jahanshahloo. I think a custom link component that uses the useRouter hook is a good solution if every thing is navigable by links. Unfortunately in a web browser this is not true. Nextjs 12 router events take into account navigation from the browser back and forward buttons. I'm looking for a solution that handles both scenariosYowl
M
0

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

Merriment answered 25/10, 2023 at 2:1 Comment(1)
Hey @truongthi-bic, thanks for your answer. So I tested your solution and the useEffect is running after the page change. So it works for an effect after the pathname change which is great, but I am looking for a solution that tells me right before the route changes and the page renders.Yowl
M
0

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.

Mariken answered 6/2 at 0:3 Comment(0)
V
-2
"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!

Verbenaceous answered 30/1 at 17:13 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Delanos

© 2022 - 2024 — McMap. All rights reserved.