1
votes

I use Angular 9 I have Parent and Child components and service which stores BehaviorSubject. All components have OnPush strategy

  1. Parent component uses async pipe to subscribe on that service subject
  2. Parent component contains child component
  3. Child component changes that service subject at the beginning of its lifecycle (ngOnInit)

With this steps parent will not react immediately after child changes the observable. Here is reproducible demo . Parent will not be updated until you focus input of a child, for instance.

I have debugged the process and see that tick function is called after async pipe does markForCheck. So I cannot understand why then the view is not updated? I notice that if I Change strategy back to Default the ExpressionChangedAfterItHasBeenCheckedError will appear, so it looks like that this change happens outside of main Change Detection process.

Can you help me what is the problem with current approach and what is the correct way to make changes like I described?

2

2 Answers

2
votes

The problem is in data flow, when an OnPush component is in the middle of render its state (class properties) should be stable, but the approach in your example breaks it because an observable value had been changed in the middle of render whereas its pointer item$ stayed the same and for OnPush it meant no reason to rerender async.

Therefore possible solutions are:

  • manually to tell angular that the component has been updated
  • to cause changes when the component is stable
  • to avoid onPush to let angular do the deep check to detect the changed observable value.

you can force check in ngAfterContentInit of the parent component once its content has been rendered and the component is stable.

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements AfterContentInit {
  constructor(private iService: ItemsService, private cdr: ChangeDetectorRef) { }
    item$ = this.iService.item$;

    ngAfterContentInit() { // <- add this
      this.cdr.detectChanges();
      this.cdr.markForCheck();
    }
}

Here is the demo: https://stackblitz.com/edit/angular-ivy-pxxiee?file=src%2Fapp%2Fcomponents%2Finner%2Finner.component.ts


If you don't want to use ngAfterContentInit, then you can defer emits making them being issued after all lifecycle hooks.

    item$ = this.itemSubject.asObservable().pipe(
      delay(0),
    );

Here is the demo: https://stackblitz.com/edit/angular-ivy-v7upxo?file=src/app/services/items.service.ts

0
votes

This problem occurs, because you parent component needed your child component to initialize property before it can mark its view as initialized. Before parent's initialization could complete, you changed the value displayed in the template. That's why when you move to default strategy, you get expression has changed error.

For bypassing this put your value changing statement which is in ngOnInit into a setTimeout, so that first parent could complete its initialization then you emit that value.

Note :- I am saying setTimeout just to observe the behavior. Not as a solution.