nextjs-nav-guard

Prevent accidental navigation away from unsaved changes in Next.js App Router. Zero config. Two lines of code.

npm versionlicense

Install

npm install nextjs-nav-guard

What it intercepts

Router methods

router.push(), router.replace(), router.refresh()

Link clicks

Next.js <Link> and plain <a> tags

Browser navigation

Back/forward buttons, history.go()

Page unload

Tab close, window.location changes

Quick Start

1. Wrap your app with the provider in your root layout:

// app/layout.tsx
import { NavigationGuardProvider } from "nextjs-nav-guard";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NavigationGuardProvider>{children}</NavigationGuardProvider>
      </body>
    </html>
  );
}

2. Use the hook in any component with unsaved changes:

import { useNavigationGuard } from "nextjs-nav-guard";

function MyForm() {
  const [isDirty, setIsDirty] = useState(false);

  useNavigationGuard({
    enabled: isDirty,
    confirm: () => window.confirm("You have unsaved changes. Leave anyway?"),
  });

  return <form>{/* your form */}</form>;
}

That's it. Two imports, two lines of setup.

Custom Dialog UI

Omit the confirm callback to use async mode. The hook returns active, accept, and reject so you can render your own confirmation dialog:

import { useNavigationGuard } from "nextjs-nav-guard";

function MyForm() {
  const [isDirty, setIsDirty] = useState(false);
  const guard = useNavigationGuard({ enabled: isDirty });

  return (
    <>
      <form>{/* your form */}</form>

      {guard.active && (
        <Dialog open>
          <p>You have unsaved changes. Leave anyway?</p>
          <button onClick={guard.reject}>Stay</button>
          <button onClick={guard.accept}>Leave</button>
        </Dialog>
      )}
    </>
  );
}

Conditional Guard

The enabled option accepts a function that receives the navigation type, so you can guard selectively:

useNavigationGuard({
  enabled: ({ type }) => {
    // Only guard against link clicks and back/forward, not refresh
    return type !== "refresh" && type !== "beforeunload";
  },
  confirm: () => window.confirm("Discard changes?"),
});

API Reference

<NavigationGuardProvider>

Wrap your app with this provider in your root layout. No props required other than children. It sets up interception of all navigation methods listed above.

useNavigationGuard(options)

Register a navigation guard. Returns an object with active, accept, and reject.

Options

OptionTypeDefaultDescription
enabledboolean | (params) => booleantrueWhether the guard is active. Can be a function receiving navigation params.
confirm(params) => boolean | Promise<boolean>undefinedConfirmation callback. Return true to allow, false to block. If omitted, uses async mode.
disableForTestingbooleanfalseMakes the hook a no-op. No provider required. Use in tests and Storybook.

Return Value

PropertyTypeDescription
activebooleantrue when a navigation attempt is pending confirmation (async mode only).
accept() => voidAllow the pending navigation.
reject() => voidBlock the pending navigation.

Navigation Params

Both enabled (when a function) and confirm receive:

PropertyTypeDescription
tostringThe target URL.
type"push" | "replace" | "refresh" | "popstate" | "beforeunload"How the navigation was triggered.

Type Exports

import type {
  NavigationGuard,         // (params: NavigationGuardParams) => boolean | Promise<boolean>
  NavigationGuardOptions,  // { enabled?, confirm?, disableForTesting? }
  NavigationGuardParams,   // { to: string; type: "push" | "replace" | ... }
} from "nextjs-nav-guard";

Compatibility

Next.jsReactStatus
14.x18, 19Supported
15.x18, 19Supported
16.0 – 16.2+19Supported

Migrating from next-navigation-guard

The API is identical. Just change the import:

- import { NavigationGuardProvider, useNavigationGuard } from "next-navigation-guard";
+ import { NavigationGuardProvider, useNavigationGuard } from "nextjs-nav-guard";

If you were using Pages Router, you'll need to switch to App Router — Pages Router support has been removed.