Issue:
After removing zone.js
from my Angular 18 project to boost performance, I'm encountering an inconsistency with the ngAfterViewInit
lifecycle hook in a component utilizing ChangeDetectionStrategy.OnPush
. Currently, I am resorting to using ApplicationRef.tick()
as a workaround, but I seek a more suitable solution aligning with the reactive paradigm without manual change detection triggers.
Scenario:
- Angular version: 18.0.6
- Successful removal of
zone.js
led to unexpected behavior in lifecycle hooks. - The admin component structure employs Angular Material's
MatSidenav
, managed through 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>
In my AdminComponent
, the ngAfterViewInit()
method only executes after explicitly calling this.appRef.tick()
. However, I aim to avoid relying on ApplicationRef
for this purpose and instead seek a solution allowing Angular to manage updates effortlessly without reverting to zone.js
.
Challenge:
- The
ngAfterViewInit()
functionality does not activate as intended without manual intervention viaApplicationRef.tick()
. - This complication arises post the removal of
zone.js
, hinting at a hurdle with Angular's change detection mechanism in a zone-less setting.
Inquiry:
How can I warrant that lifecycle hooks like ngAfterViewInit
are appropriately triggered in an Angular application featuring ChangeDetectionStrategy.OnPush
post the elimination of zone.js
? Are there any endorsed practices or patterns for efficiently handling change detection manually in such instances?
Tried Solutions:
- Employing
ApplicationRef.tick()
to manually initiate change detection. - Subscribing to router events and invoking
within subscriptions.ChangeDetectorRef.detectChanges()
- Utilizing
NgZone.run()
for executing code updating the view.
None of these techniques have seamlessly integrated into the intended reactive architecture. I seek a more unified or Angular-recommended approach augmenting performance while upholding reactivity and maintainability.