You use Next.js Pages Router, and you want to show "You have unsaved changes that will be lost." dialog when user presses back button? This library is just for you!
This package is based on next-navigation-guard, but has been rebuilt from scratch to focus solely on back navigation support for Pages Router only.
Please refer to the docs for detailed information.
| # | Document | Description |
|---|---|---|
| 01 | Why This Library | Core problems and solutions (URL restoration, index tracking, session tokens) |
| 02 | Blocking Scenarios | All back navigation scenarios with flow diagrams |
| 03 | Priority System | Handler options: override, enable, once, conflict detection |
| 07 | Limitations | Known limitations and non-working cases |
For contributors: Design Evolution | Internal Implementation | preRegisteredHandler Reference Stability
Try it out: Live Demo
Note: This package is not yet published to npm. Coming soon!
Pages Router: pages/_app.tsx
import { BackNavigationHandlerProvider } from "next-page-router-back-navigation-guard";
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<BackNavigationHandlerProvider>
<Component {...pageProps} />
</BackNavigationHandlerProvider>
);
}import { useRegisterBackNavigationHandler } from "next-page-router-back-navigation-guard";
function MyComponent() {
useRegisterBackNavigationHandler(() => {
return window.confirm("You have unsaved changes that will be lost.");
});
return <YourContent />;
}Register a handler for back navigation (browser back button, router.back()).
useRegisterBackNavigationHandler(
() => {
// Return true to allow navigation
// Return false to block navigation
return window.confirm("Leave page?");
},
{
once: false, // If true, handler executes once then unregisters (regardless of return value)
enable: true, // If false, handler is not registered
override: false, // If true, handler has priority over non-override handlers
}
);Note on
onceoption: Whenonce: true, the handler is removed immediately upon execution, regardless of whether it returnstrueorfalse. This means "execute exactly once", not "allow navigation once".
Provider component that enables back navigation handling.
<BackNavigationHandlerProvider
preRegisteredHandler={() => {
// Optional: runs FIRST with highest priority
// Return false to block, true to allow
if (isGlobalModalOpen) {
closeModal();
return false;
}
return true;
}}
>
<App />
</BackNavigationHandlerProvider>| Navigation Type | Intercepted? |
|---|---|
| Browser back button | Yes |
router.back() |
Yes |
| Browser forward button | No |
router.push/replace |
No |
<Link> clicks |
No |
| Tab close/reload | No |
This library uses E2E tests only (no unit tests) with Playwright. Tests run against Chromium, Firefox, and WebKit.
# Install Playwright browsers (first time only)
pnpm e2e:install
# Run all tests
pnpm e2e
# Run with Playwright UI
pnpm e2e:ui| Test Suite | Description |
|---|---|
| Basic Handler | Dialog show, block on cancel, allow on confirm |
| Once Option | Handler executes once then auto-unregisters (regardless of return value) |
| Enable Option | Conditional handler registration (enable/disable toggle) |
| Override Handlers | Priority handlers execute before normal handlers |
| Priority Order | Lower priority number (0) runs before higher (1, 2, 3) |
| Pre-registered Handler | Handler registered via Provider's preRegisteredHandler prop |
| Pre-registered Handler (Overlay Close) | preRegisteredHandler closes overlay and blocks navigation |
| Browser Back Button | page.goBack() triggers handler same as router.back() |
| After Refresh (Token Mismatch) | Handler works correctly after page reload |
Firefox requires special Playwright configuration due to a known bug where page.goBack() doesn't trigger popstate events after page.reload():
// playwright.config.ts
{
name: "firefox",
use: {
launchOptions: {
firefoxUserPrefs: {
"fission.webContentIsolationStrategy": 1, // Fix for goBack after reload
},
},
},
}Reference: microsoft/playwright#23210
See Limitations for known limitations including:
- Requires history within the same app (doesn't work on first page entry)
- Do NOT use
router.push/replaceinside handler — causes unpredictable navigation behavior due to browser security policies - Samsung Internet Browser "Block backward redirections" feature (enabled by default)
Try the Live Demo or see the source code in example/ directory.