nextjs-nav-guard
v1.0.9
Published
Prevent accidental navigation away from unsaved changes in Next.js App Router.
Downloads
10,057
Maintainers
Readme
nextjs-nav-guard
Navigation guard for Next.js App Router. Prevent users from accidentally leaving pages with unsaved changes.
This is a maintained fork of next-navigation-guard by LayerX, which is no longer actively maintained and incompatible with recent Next.js versions.
What's different from the original?
- Next.js 16.2+ support: fixed a runtime crash caused by
nullhistory.statein newer Next.js versions - Resilient internals: fragile
next/dist/*imports are centralized behind try/catch fallbacks, so the library won't crash if Next.js moves internal APIs - Fixed React hooks violation: removed a conditional
useContextcall that broke the rules of hooks - Pages Router removed: focused on App Router only (Pages Router is deprecated)
- Better error messages: actionable errors when the provider is missing, with code examples
- Actively maintained: compatible with Next.js 14, 15, and 16 (including 16.2+)
Install
npm install nextjs-nav-guardSetup
Wrap your app with NavigationGuardProvider 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>
);
}Usage
Simple: window.confirm()
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>;
}Custom dialog UI
If you want full control over the confirmation UI, omit the confirm callback. The hook returns active, accept, and reject to drive your own 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 with navigation type
The enabled option can be a function that receives the navigation type, letting you 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
<NavigationGuardProvider>
Wrap your app with this provider. It intercepts navigation at multiple levels:
- Router methods:
router.push(),router.replace(),router.refresh() - Link clicks:
<Link>and<a>tag clicks - Browser navigation: back and forward buttons,
history.go() - Page unload: tab close,
window.locationchanges
useNavigationGuard(options)
Register a navigation guard. Returns { active, accept, reject }.
Options
| Option | Type | Default | Description |
|---|---|---|---|
| enabled | boolean \| (params) => boolean | true | Whether the guard is active. Can be a function receiving { to, type } |
| confirm | (params) => boolean \| Promise<boolean> | undefined | Sync or async confirmation callback. Return true to allow, false to block. If omitted, the hook uses async mode |
| disableForTesting | boolean | false | Makes the hook a complete 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
The enabled function and confirm callback both receive:
| Property | Type | Description |
|---|---|---|
| to | string | The target URL |
| type | "push" \| "replace" \| "refresh" \| "popstate" \| "beforeunload" | How the navigation was triggered |
NavigationGuard (type export)
The callback type, exported for convenience:
import type { NavigationGuard } from "nextjs-nav-guard";
const myGuard: NavigationGuard = ({ to, type }) => {
return window.confirm(`Navigate to ${to}?`);
};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.
Limitations
Reloads and tab closes use the browser dialog
Custom dialog UIs only work for client-side navigations. When the browser fires beforeunload (page reload, tab close, leaving for another site), browsers do not allow async work or custom UI. The library can only request the browser's built-in confirmation dialog, and its text and appearance cannot be customized.
Direct History API calls are not guarded
Calls made directly through window.history.pushState() or window.history.replaceState() bypass the guard. If you call either method yourself, confirm the navigation before calling it.
Guarded link clicks are handled programmatically
To intercept <Link> and <a> clicks, the provider registers a capture-phase click handler. While a guard is enabled, that handler prevents the original click and stops its propagation while the confirmation is pending, then navigates via the App Router if accepted. Code that relies on that click's normal propagation may need to account for this.
Next.js 16.2 drops query-only replacements after an async guard
Next.js 16.2 has an App Router regression: after an async guard is accepted, a router.replace() that changes only the query string may be dropped. This is a Next.js bug, fixed in Next.js 16.3.0, and the library cannot safely work around it. If your app uses this pattern, stay on 16.1.x or upgrade to Next.js 16.3.0 or later.
Compatibility
| Next.js | React | Status | |---|---|---| | 14.x | 18, 19 | Supported | | 15.x | 18, 19 | Supported | | 16.x | 19 | Supported (16.2 has a known Next.js router bug, see Limitations) |
License
MIT license. Originally created by LayerX Inc.
