nextjs-nav-guard
Prevent accidental navigation away from unsaved changes in Next.js App Router. Zero config. Two lines of code.
Install
npm install nextjs-nav-guardWhat 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
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | (params) => boolean | true | Whether the guard is active. Can be a function receiving navigation params. |
confirm | (params) => boolean | Promise<boolean> | undefined | Confirmation callback. Return true to allow, false to block. If omitted, uses async mode. |
disableForTesting | boolean | false | Makes the hook a no-op. No provider required. Use in tests and Storybook. |
Return Value
| Property | Type | Description |
|---|---|---|
active | boolean | true when a navigation attempt is pending confirmation (async mode only). |
accept | () => void | Allow the pending navigation. |
reject | () => void | Block the pending navigation. |
Navigation Params
Both enabled (when a function) and confirm receive:
| Property | Type | Description |
|---|---|---|
to | string | The 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.js | React | Status |
|---|---|---|
| 14.x | 18, 19 | Supported |
| 15.x | 18, 19 | Supported |
| 16.0 – 16.2+ | 19 | Supported |
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.