I've recently removed zone.js
from my Angular 18 project to optimize performance and am now facing an issue with the ngAfterViewInit
lifecycle hook not firing consistently in a component with ChangeDetectionStrategy.OnPush
. My current workaround involves using ApplicationRef.tick()
, but I'm looking for a more appropriate solution that aligns with the reactive paradigm and does not rely on manual change detection triggers.
Context:
zone.js
, but this has led to lifecycle hooks not being triggered as expected.MatSidenav
, controlled via reactive signals from @angular/core
.admin.component.ts:
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export default class AdminComponent implements OnDestroy, AfterViewInit {
sidenav = viewChild.required<MatSidenav>(MatSidenav);
private destroy$ = new Subject<void>();
private observer = inject(BreakpointObserver);
private router = inject(Router);
private accountService = inject(AccountService);
private cdr = inject(ChangeDetectorRef);
private appRef = inject(ApplicationRef);
constructor() {
this.router.events.pipe(
filter(e => e instanceof NavigationEnd), takeUntil(this.destroy$)
).subscribe(() => {
this.appRef.tick();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit() {
this.observer
.observe(['(max-width: 800px)'])
.pipe(takeUntil(this.destroy$), delay(1))
.subscribe((res: { matches: boolean }) => {
this.setSidenav(res.matches);
this.cdr.detectChanges();
});
this.router.events
.pipe(
takeUntil(this.destroy$),
filter((e) => e instanceof NavigationEnd)
)
.subscribe(() => {
if (this.sidenav().mode === 'over') {
this.setSidenav(false);
}
});
}
logout() {
this.accountService.logout();
}
private setSidenav(matches: boolean) {
if (matches) {
this.sidenav().mode = 'over';
this.sidenav().close();
} else {
this.sidenav().mode = 'side';
this.sidenav().open();
}
}
}
admin.component.html:
<mat-toolbar color="primary" class="mat-elevation-z8">
<button mat-icon-button *ngIf="sidenav.mode === 'over'" (click)="sidenav.toggle()">
<mat-icon *ngIf="!sidenav.opened">menu</mat-icon>
<mat-icon *ngIf="sidenav.opened">close</mat-icon>
</button>
<span class="admin-panel-title">Admin Panel</span>
</mat-toolbar>
<mat-sidenav-container>
<mat-sidenav #sidenav="matSidenav" class="mat-elevation-z8">
<div class="logo-container">
<img routerLink="/" alt="logo" class="avatar mat-elevation-z8 logo-admin" src="../../../../assets/img/logo-s.png" />
</div>
<mat-divider></mat-divider>
<ul class="nav-list">
<li class="nav-item">
<a routerLink="/admin" routerLinkActive="active" [routerLinkActiveOptions]="{exact:true}">
<mat-icon class="nav-icon">home</mat-icon>
<span>Dashboard</span>
</a>
</li>
<li class="nav-item">
<a routerLink="/admin/products" routerLinkActive="active">
<mat-icon class="nav-icon">library_books</mat-icon>
<span>Products</span>
</a>
</li>
<li class="nav-item">
<a routerLink="/admin/brands" routerLinkActive="active">
<mat-icon class="nav-icon">branding_watermark</mat-icon>
<span>Brands</span>
</a>
</li>
<li class="nav-item">
<a routerLink="/admin/product-types" routerLinkActive="active">
<mat-icon class="nav-icon">branding_watermark</mat-icon>
<span>Types</span>
</a>
</li>
<li class="nav-item">
<a routerLink="/admin/users" routerLinkActive="active">
<mat-icon class="nav-icon">supervisor_account</mat-icon>
<span>Users</span>
</a>
</li>
<li class="nav-item">
<a (click)="logout()" class="nav-item-logout" routerLinkActive="active">
<i class="fa fa-sign-out fa-2x nav-icon-logout"></i>
<span>Logout</span>
</a>
</li>
</ul>
<mat-divider></mat-divider>
</mat-sidenav>
<mat-sidenav-content>
<div class="content mat-elevation-z8">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
The ngAfterViewInit()
in my AdminComponent
only logs when I explicitly call this.appRef.tick()
. However, I would prefer not to use ApplicationRef
for this purpose and am looking for a solution that allows Angular to handle updates more naturally without reverting to zone.js
.
Problem:
ngAfterViewInit()
method is not triggering as expected without manual intervention via ApplicationRef.tick()
.zone.js
, indicating a challenge with Angular's change detection mechanism in a zone-less environment.Question:
How can I ensure that lifecycle hooks like ngAfterViewInit
are triggered appropriately in an Angular application with ChangeDetectionStrategy.OnPush
when zone.js
is removed? Are there any recommended practices or patterns for managing change detection manually in such cases?
What I've Tried:
ApplicationRef.tick()
to manually trigger change detection.ChangeDetectorRef.detectChanges()
within subscriptions.NgZone.run()
for executing code that updates the view.None of these methods have seamlessly integrated into the reactive architecture I aim for. I'm looking for a more integrated or Angular-recommended approach that enhances performance without compromising reactivity and maintainability.
I found the solution to my issue. The problem was related to running my Angular application without zone.js
and using Angular Material components, specifically MatSidenav
.
To resolve this, I needed to use the provideExperimentalZonelessChangeDetection()
function in the providers array of my AppModule
. This setup ensures that Angular can handle change detection properly in a zoneless configuration.
AppModule
:
app.module.ts
import { NgModule, provideExperimentalZonelessChangeDetection } from
@NgModule({
declarations: [
AppComponent,
],
bootstrap: [AppComponent],
imports: [
],
providers: [
provideExperimentalZonelessChangeDetection(), // Add this line
]
})
export class AppModule { }
provideExperimentalZonelessChangeDetection()
to the providers array, Angular is configured to handle change detection properly without zone.js
.MatSidenav
, which rely on change detection to function correctly.After making this change, my application works perfectly without zone.js
.