Events Substitution in App Routing for NextJS v13

Before Next.js 13, Pages Router was the only solution based on traditional file-system routing and client-side navigation. Starting with Next.js 13, App Routing was implemented as a default replacement.

App Router uses a component-based approach with a focus on Server Components and nested layouts, but in projects based on the Pages Router developers had access to Router.events which exposed routing life-cycle events like routeChangeStart, routeChangeComplete, and routeChangeError. They give you full control over navigation flow: you could show global spinners, log transitions, or (one of most valuable features) cancel navigation when there were unsaved changes.

App Router doesn’t expose any events and moreover affects native events so we as developers are missing an important tool. In this blog, I’ll show you the key changes introduced in Next.js 13’s App Router, explain the challenges that come with the removal of Router.events, and walk you through practical workarounds to restore navigation event handling and functionality.

Changes in NextJS v13 Routing

With the App Router, navigation is no longer just a wrapper around history.pushState and popstate. Instead, Next.js maintains a complex router state machine backed by React Server Components. This state is synchronized with the browser’s history entries, but the history API is essentially hidden behind Next’s own abstractions.

As part of this change:

  • next/router and Router.events are no longer available in App Router projects.
  • browser events like popstate are intercepted internally by Next.js and may never reach your code.
  • navigation detection now relies on reactive hooks such as usePathname() and useSearchParams().

This ensures consistency for Server Components and data streaming, but takes away the ability to listen to “before navigation” events. That’s why Next.js intercepts popstate and hides low-level events – if developers could cancel navigation arbitrarily, it could desync the router from the streamed server payloads.

What’s Complicated by These Changes

  • Global loading indicators – without routeChangeStart and routeChangeComplete, it is unclear when to show a spinner during navigation.
  • Navigation guards – the classic “unsaved changes, are you sure you want to leave?” modal is no longer possible as we used to understand them. At best you can revert after the navigation is already happened.
  • Back/Forward button handlingwindow.addEventListener("popstate") does not fire reliably (in my practice – it doesn’t fire at all), because Next.js consumes the event internally.
  • Navigation cancellation – even if you detect a navigation, you cannot prevent it before Next renders the new route

Workaround from the Official Documentation

The official recommendation is to use React hooks provided by next/navigation. For example:

// app/components/navigation-events.js

'use client'
 
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
 
export function NavigationEvents() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
 
  useEffect(() => {
    const url = `${pathname}?${searchParams}`
    console.log(url)
    // You can now use the current URL
    // ...
  }, [pathname, searchParams])
 
  return '...'
}

// app/layout.js

import { Suspense } from 'react'
import { NavigationEvents } from './components/navigation-events'
 
export default function Layout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
 
        <Suspense fallback={null}>
          <NavigationEvents />
        </Suspense>
      </body>
    </html>
  )
}

As we can see, here we can only react to changes in pathname. This is reliable for knowing after a navigation completes, but it doesn’t tell you when it started, nor does it let you stop it.

Events Substitution Workarounds in App Routing

Main directions for these cases are to prevent away navigation if some changes are not saved and to listen navigation events for make UI more friendly and informative.

!Important
This solution doesn’t work:

useEffect(() => {
  const handler = () => {
    console.log("routeChangeStart (back/forward)");
  };
  window.addEventListener("popstate", handler);
  return () => window.removeEventListener("popstate", handler);
}, []);

That happens because the browser does dispatch popstate when you hit Back/Forward but Next.js App Router attaches its own listener very early (before your app code runs). Inside app-router.client.ts, it consumes that event and sometimes calls event.stopImmediatePropagation() which that means your later window.addEventListener(“popstate”) never runs.

1. Monkey-patching the History API to Restore Navigation Events

By wrapping history.pushState, history.replaceState, and ensuring early popstate listeners are attached in the capture phase, you can dispatch your own custom events that mimic routeChangeStart and routeChangeComplete:

// For purpose of simpliсity this code given as a function
// We can move it to custom hook or into useLayoutEffect in root App component

function patchHistory() {
  const rawPush = history.pushState;
  const rawReplace = history.replaceState;

  history.pushState = function (...args: any[]) {
    const result = rawPush.apply(this, args as any);
    window.dispatchEvent(new Event("locationchange"));
    return result;
  };

  history.replaceState = function (...args: any[]) {
    const result = rawReplace.apply(this, args as any);
    window.dispatchEvent(new Event("locationchange"));
    return result;
  };

  const rawAdd = window.addEventListener;
  window.addEventListener = function (type, listener, options) {
    if (type === "popstate") {
      return rawAdd.call(this, type, listener, { capture: true });
    }
    return rawAdd.call(this, type, listener, options);
  };

  window.addEventListener("popstate", () => {
    window.dispatchEvent(new Event("locationchange"));
  });
}

patchHistory();

Now you can listen for locationchange in your app and respond to all navigation, including Back/Forward.

2. Reverting Navigation to Do Cancel-Like Behaviour

Since you cannot cancel navigation before it happens, the option is to implement a watcher to detect the change and immediately restore the old route (extended example from above) and run another helpful logic:

"use client";

import { usePathname, useRouter } from "next/navigation";
import { useRef, useEffect } from "react";

export function NavigationWatcher() {
  const pathname = usePathname();
  const router = useRouter();
  const prevPath = useRef(pathname);

  useLayoutEffect(() => {
    if (pathname !== prevPath.current) {
      // Simplified UI flow just  to make it work. We can build complex and beauty UI flow here
      const confirmLeave = window.confirm("Leave this page?");
      if (!confirmLeave) {
        router.push(prevPath.current);
        return;
      }
    }
    prevPath.current = pathname;
  }, [pathname, enabled, router]);
  
  return null;
}

and we can use it:

import { Suspense } from 'react';
import { NavigationWatcher } from './components/navigation-watcher';
 
export default function Layout({ children }) {
	return (
		<html lang="en"> 
			<body> 
				{children}
				<Suspense fallback={null}>
					<NavigationWatcher />
				</Suspense>
			</body>
		</html>
	)
}

!Important

Component NavigationWatcher should not be unmount on navigation.
It should observe navigation throughout all lifespan of the application so we place it in root layout component.
And one more thing (taken from the documentation):

<NavigationEvents> is wrapped in a Suspense boundary because useSearchParams() causes client-side rendering up to the closest Suspense boundary during static renderingLearn more.

From a user perspective this feels almost like a blocked navigation, though technically the URL changes for a split second before being reverted.

3. Implement a Guard to Do Cancel-Like Behaviour

Another possible solution is to wrap possible target pages (or wrap root layout or modify logic of existing guards) with a custom guard to restore the old route:

function randomPageGuard<P extends object> (Component: ComponentType<P>)  {
    return function manageNavigation(props: P) {

        const pathname = usePathname();
        const router = useRouter();
        
        // Hardcoded value or result of some logic
        const previousRoute = '/previous-route';

        // Some condition goes here
        const canNavigate = false;

        if (!canNavigate) {
            router.push(previousRoute);
            return null;
        }

        return (<Component {...props} />);
    };
}

Despite these workarounds, there are still caveats:

  • No true cancellation – navigation always happens before your code runs. You can only undo it afterwards.
  • Hydration mismatch risk – aggressive monkey-patching of history may desync Next’s internal state if their implementation changes in future versions.
  • Reloads and direct URL typing – when the user enters a new address or reloads the page, this always triggers a full server reload. The only option is the beforeunload event, which shows a generic browser warning.
  • Future breakage – since these workarounds depend on internals, they may stop working if Next.js changes how it manages history.
  • Last but important – when back redirection happens in fact the page is loading one more time. So if there will be some initial logic like API calls or visual effects like animations – they will be start again. In that case we have to check is that logic should be executed and prevent unwanted effects

Conclusion

With the App Router, Next.js has moved routing control deeper into React and its server components system. The upside is powerful streaming and cache management, but the downside is the loss of low-level navigation hooks. For now, developers who need global indicators or guards must rely on monkey-patches and post-navigation checks.

It is known that the community has raised several questions regarding this gap, and it’s possible that a future release will reintroduce a supported router.events-like API.

If you’d like the help of React and Next.js experts on your modern web application, contact us at Trailhead!

Picture of Alex Movchan

Alex Movchan

Alex started his professional path in finance and banking and devoted at least 15 years to the financial industry before switching to software engineering. Now he has over 7 years of experience as a full-stack developer of web applications. Alex is experienced in all the popular front-end Javascript frameworks, TypeScript, and plain old Javascript itself. He is also closely familiar with NodeJS as a backend technology. His academic background gives him an advantage in algorithms, statistics, and data analysis. Alex likes a result-oriented approach to software development, which he likes to execute in a reliable and solution-oriented manner. He likes to be up-to-date with the latest trends and best practices and even enjoys learning a little in connected spheres such as mobile development.

Free Consultation

Sign up for a FREE consultation with one of Trailhead's experts.

"*" indicates required fields

This field is for validation purposes and should be left unchanged.

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.