Angular 18: How to Ensure ngAfterViewInit Fires without zone.js in OnPush Components

  Kiến thức lập trình

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 via ApplicationRef.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:

  1. Using ApplicationRef.tick() to manually trigger change detection.
  2. Subscribing to router events and calling ChangeDetectorRef.detectChanges() within subscriptions.
  3. 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.

Theme wordpress giá rẻ Theme wordpress giá rẻ Thiết kế website

LEAVE A COMMENT