Body:
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:
- Angular version: 18.0.6
- I’ve successfully removed
zone.js
, but this has led to lifecycle hooks not being triggered as expected. - The application structure involves an admin component that uses Angular Material’s
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:
- The
ngAfterViewInit()
method is not triggering as expected without manual intervention viaApplicationRef.tick()
. - This issue occurs only after removing
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:
- Using
ApplicationRef.tick()
to manually trigger change detection. - Subscribing to router events and calling
ChangeDetectorRef.detectChanges()
within subscriptions. - Using
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.