I have an Angular (standalone) application with a CanDeactivate guard that prevents navigation when a form is dirty.
Reproduction steps:
Navigate to User Management
Click on a user (e.g. “Alice”)
Click Edit roles
Modify the form so it becomes dirty
Click the browser back button (Chrome)
A confirmation dialog is shown by the CanDeactivate guard.
If I click Cancel, navigation is correctly prevented and I stay on the page.
If I repeat the same action (browser back → Cancel) one more time, the browser history is now empty.
After that, the browser back button no longer works (history length is 0).
Expected behavior:
Canceling navigation via CanDeactivate should not consume browser history entries, even when done multiple times.
Actual behavior:
Each canceled browser back navigation seems to consume one history entry.
Guard implementation:
const canLeaveRoles: CanDeactivateFn = (component) => {
if (!component.dirty) return true;
return confirm('You have unsaved changes. Leave anyway?');
};
Notes:
In-app back buttons use
Location.back()The issue occurs only when using the browser back button
This happens consistently in Chrome
Question:
Is this expected behavior of Angular Router / browser history?
If so, what is the recommended way to prevent browser history from being consumed when navigation is canceled?
stackblitz demo
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
Routes,
provideRouter,
RouterOutlet,
RouterLink,
ActivatedRoute,
CanDeactivateFn,
} from '@angular/router';
import { Location, NgFor } from '@angular/common';
import { FormsModule } from '@angular/forms';
/* -------------------- App Root + Sidenav -------------------- */
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
`,
styles: [
`
.layout { display: flex; height: 100vh; font-family: Arial; }
.sidenav { width: 200px; padding: 16px; border-right: 1px solid #ddd; background: #f7f7f7; }
.sidenav a { display: block; padding: 8px 0; text-decoration: none; color: #333; }
.sidenav a.active { font-weight: bold; }
.content { padding: 16px; flex: 1; }
`,
],
})
class AppComponent {}
/* -------------------- Management -------------------- */
@Component({
standalone: true,
imports: [NgFor, RouterLink],
template: `
User Management
`,
})
class ManagementComponent {
users = [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
{ id: '3', name: 'Charlie' },
];
}
/* -------------------- User -------------------- */
@Component({
standalone: true,
imports: [RouterLink],
template: `
User {{ userId }}
Roles
Edit roles
`,
})
class UserComponent {
userId = this.route.snapshot.paramMap.get('userId');
constructor(private route: ActivatedRoute, private location: Location) {}
back() {
this.location.back();
}
}
/* -------------------- Roles -------------------- */
@Component({
standalone: true,
imports: [FormsModule],
template: `
Edit Roles
`,
})
class RolesComponent {
dirty = false;
roles = {
admin: false,
editor: true,
};
constructor(private location: Location) {}
markDirty() {
this.dirty = true;
}
save() {
this.dirty = false;
this.location.back();
}
cancel() {
this.location.back();
}
}
/* -------------------- Guard -------------------- */
const canLeaveRoles: CanDeactivateFn = (component) =>
!component.dirty || confirm('You have unsaved changes. Leave anyway?');
/* -------------------- Routes (with titles) -------------------- */
const routes: Routes = [
{
path: '',
redirectTo: 'management',
pathMatch: 'full',
},
{
path: 'management',
component: ManagementComponent,
title: 'User Management',
},
{
path: 'management/:userId',
component: UserComponent,
title: (route) => `User ${route.paramMap.get('userId')}`,
},
{
path: 'management/:userId/roles',
component: RolesComponent,
title: 'Edit User Roles',
canDeactivate: [canLeaveRoles],
},
];
/* -------------------- Bootstrap -------------------- */
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)],
});