简体   繁体   中英

Angular async pipe in combination with Router shows different behavior in build for development and in build for production

I have a very simple test app made of 2 components

  • AppComponent
  • ListComponent

Both components have the same behavior: at load time they show a list of items coming from an Observable using async pipe. AppComponent in addition has a button and a RouterOutlet where it loads ListComponent as soon as the button is clicked.

When this code is compiled for development, ie with ng build , everything works as expected. When the same code is compiled for prod, ie ng build --prod , then the behavior is different. In this second case, if I click the button to go to ListComponent , the Observable of ListComponent DOES NOT EMIT anymore at the load of the page.

The code is the following (I have also a Stackblitz example where the problem though does not occur)

ListComponent

@Component({
  selector: 'app-list',
  template: `
    <input #searchField type="text">
    <div *ngFor="let item of itemsToShow$ | async">{{item}}</div>
  `,
})
export class ListComponent implements OnInit, AfterViewInit  {
  @ViewChild('searchField', {static: true}) searchField: ElementRef;
  search$: Observable<string>;
  itemsToShow$: Observable<string[]>;

  ngOnInit() {}

  ngAfterViewInit() {
    this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
      map(() => this.searchField.nativeElement.value)
    );

    this.itemsToShow$ = this.itemsToShow();
  }

  itemsToShow() {
    let itemAsLocalVar: string[];
    return of(ITEMS).pipe(
      delay(10),
      tap(items => itemAsLocalVar = items),
      switchMap(() => combineLatest([this.search$])),
      map(([searchFilter]) => itemAsLocalVar.filter(i => i.includes(searchFilter))),
    );
  }
}

AppComponent

@Component({
  selector: 'app-root',
  template: `
    <input #searchField type="text">
    <div *ngFor="let item of itemsToShow$ | async">{{item}}</div>
    <router-outlet></router-outlet>
    <button (click)="goToList()">List</button>
  `
})
export class AppComponent implements OnInit, AfterViewInit  {
  @ViewChild('searchField', {static: true}) searchField: ElementRef;
  search$: Observable<string>;
  itemsToShow$: Observable<string[]>;

  constructor(
    private router: Router,
  ) {}

  ngOnInit() {}

  ngAfterViewInit() {
    this.search$ = merge(concat(
       of(''), 
       fromEvent(this.searchField.nativeElement, 'keyup')
     )).pipe(
       map(() => this.searchField.nativeElement.value)
     );
    this.itemsToShow$ = this.itemsToShow();
  }
  itemsToShow() {
    let itemAsLocalVar: string[];
    return of(ITEMS).pipe(
      delay(10),
      tap(items => itemAsLocalVar = items),
      switchMap(() => {
        return combineLatest([this.search$]);
      }),
      map(([searchFilter]) => itemAsLocalVar.filter(i => i.includes(searchFilter))),
      tap(i => console.log(i))
    );
  }
  goToList() {
    this.router.navigate(['list']);
  }
}

Any idea of what is going wrong is very much appreciated.

Any idea of

A strange/weird behaviour but good that you posted this issue/question.

I think the issue in production mode is happening because the way change detection works in production vs development mode [ https://blog.angularindepth.com/a-gentle-introduction-into-change-detection-in-angular-33f9ffff6f10] .

In development mode, change detection runs two times just to be sure [because of that you might have seen Expression changed..... exception [ https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4] . Please also notice that you are setting up your observables in ngAfterViewInit . Because of 2 cycles of change detection, in dev mode, you see that ListComponent renders correctly. After the first cycle of change detection, you have assigned the observable in ngAfterViewInit which is detected in the 2nd change detection cycle and component rendered as expected.

In production mode change detection does not run two times. It runs only one time due to enhance performance. Your ListComponent will render list if you click the "List" button again [after very first click] because clicking of the button runs Angular change detection.

To fix this you have the following options -

1. Force the change detection by injecting:

constructor(private _cdref: ChangeDetectorRef) {

  }

and change your ngAfterViewInit() like this

ngAfterViewInit() {
    this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
      map(() => this.searchField.nativeElement.value)
    );
    this.itemsToShow$ = this.itemsToShow();
    this._cdref.detectChanges();
  }

2. Move your ngAfterViewInit() code to ngOnInit() like this:

ngOnInit() {

    this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
      map(() => this.searchField.nativeElement.value)
    );
    this.itemsToShow$ = this.itemsToShow();

  }

I would recommend going for option 2.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM