Reputation: 1123
I use Angular 9
I have Parent and Child components and service which stores BehaviorSubject
. All components have OnPush
strategy
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?
Upvotes: 1
Views: 1742
Reputation: 13584
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:
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
Upvotes: 2
Reputation: 10979
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.
Upvotes: 0