Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change component input from ngOnChanges, while using OnPush strategy

I'm having a problem with an Angular 6 application:

The problem

Let's say I have 2 components: parent and child.

the child has 2 inputs. While 1 input changes, inside the ngOnChanges() the child component emit somthing to the parent component.

Then the parent component changes the second input for the child component. I expect the change detection of the child component to get called once again- BUT IT'S NOT. The view of the child component shows old value of the second input.


Defenitions

the application has the following defenitions:

  1. All components use change detection strategy OnPush. that includes the root component.
  2. I'm using ngrx store. call markForCheck() or detectChanges() inside the reducer is not an option.
  3. The data goes down to the child component using async pipe, because it comes from the store using selector (observable).
  4. I can't cause the change of the second input earlier (at the parent component for example)- it has to happen after the child component recives a change of the first input.

To illustrate the case, I've created a simple demo project. There I don't use store, so I don't have any selectors.. instead I'm using rxjs Subject for the use of async pipe.

Here it is:

https://angular-ccejq4.stackblitz.io

Take a look the the console to understand what happens and when.


Bad solutions

What i've already tried:

  1. changing the change detection strategy at the root component to default. It works but this is not a good solution since it causing performance issues.
  2. call markForCheck() before and after the emit(!) - NOT WORKING.
  3. call detectchanges() before and after the emit(!) - NOT WORKING.
  4. subscribe to the selector at the parent component instead of using async pipe - NOT WORKING.
  5. call the emit from within a setTimeout - IT WORKS. but is there a better solution? a more Angular-driven one. because this solution feels more like a workaround.
  6. use ngAfterViewInit/ngAfterViewChecked and emit, call markForCheck() and detectChanges() from there - NOT WORKING.

I found that the new value arrives to the async pipe. the async pipe then calls markForCheck(). but it doesn't call detectChanges().. so a change detection cycle won't run. the view will change properly only at the next cycle of change detection- as you can see on my demo app.


Any ideas?

thanks!

like image 911
Naor Talmor Avatar asked Feb 27 '20 08:02

Naor Talmor


People also ask

How does the onpush change detection set work?

When that happens, the book is added directly to the cached list of books, or updated in place. As for the components, you have a parent component which contains a child component that lists the books and another child component that allows for the editing of those books. Those two child components are the ones with the OnPush change detection set.

How to update the value in the child component without onpush?

So now how to update the value in the child component without changing the onPush strategy, the one rule here is to always use an immutable way of passing input objects like instead of modifying object directly pass the new reference of the object. Let's modify our code accordingly in the parent component.

What is ngonchanges() in angular?

The ngOnChanges () is a built-in Angular callback method invoked immediately after the default change detector checks data-bound properties if at least one has changed. Before the view and content, children are checked. interface OnChanges { ngOnChanges ( changes: SimpleChanges): void } Let’s understand this by an example.

What is the difference between the onpush and default strategies?

The Default strategy runs every time any change happens in the app. It could be a button click, an HTTP call, a setTimeout, or any other type of timer or user interaction. The OnPush strategy on the other hand, only runs if one of four conditions are met:


1 Answers

Let me first say, great question with detail and well explained. You have indeed hit a caveat of the angular change detection.

From within ngOnChanges there is no direct way to trigger another change detection. Otherwise you will have the possibility to hit a loop. Also performance wise they do not allow this. You need to find a way to get into the next execution cycle of JS

The setTimeout is not a bad option to overcome this. You can either put this around the emit:

setTimeout(() => this.secondEmitter.emit(this.first));

or you can put it around a markForCheck call:

setTimeout(() => this.cdRef.markForCheck());

But I agree with you that it feels a bit hacky, but sometimes you just need to cope with it. If you really cannot, there is another way. Your BehaviorSubject is by nature a synchronous observable. You can however make the async subscription in your template to use the asyncScheduler:

private second = new BehaviorSubject(0);

readonly second$ = this.second.asObservable().pipe(
  observeOn(asyncScheduler)
)

constructor() {
  this.second.next(0);
  this.second$.subscribe((lastval) => {
    console.log(`second subscription update to ${lastval}`);
  });
}

From the last option I've made a fork of your stack

This will make any subscription to it receive the message after the next event loop cycle.


Update

Coming back to this, a better solution would be to use the async option on the creation of the EventEmitter.

In that case you can use .emit on the second observable:

@Output() firstEmitter:EventEmitter<number> = new EventEmitter();

// notice the 'true'
@Output() secondEmitter:EventEmitter<number> = new EventEmitter(true);

ngOnChanges(changes:any) {
  if (changes.first) {
    this.secondEmitter.emit(this.first);
  }
}

working example

like image 58
Poul Kruijt Avatar answered Oct 22 '22 21:10

Poul Kruijt