2

I'm running into a problem making my UI update in a situation where I share data between a parent directive and child components. An @Input() binding exists on the parent, and the data is then internally passed to the child components on ngOnChanges using @ContentChildren(). This way I don't have to bind my data to the parent and all child components every time I use the component.

To limit the amount of change detection cycles on the inner components, I am using the ChangeDetectionStrategy.OnPush strategy. I then trigger my own change detection when the data is passed from the parent to the children. This works great, except the change detection is now called before other data changes are processed deeper in the hierarchy. This leads to stale data that's always behind a cycle.

Here's a StackBlitz which roughly represents my current situation:
https://stackblitz.com/edit/angular-ivy-2tszsu
When updating the data, you'll see that with the OnPush strategy, the Template value is always the previous Data value.
PS. In this example data and templateData are one and the same for simplicity, but in my use case there are various unique properties.

From my understanding, here's what's happening:

  1. @Input() data on ParentComponent changes.
  2. ngOnChanges on ParentComponent fires and passes new data to ChildPushComponent using my function setData(data).
  3. ChildPushComponent fires change detection inside setData(data). This later makes the UI update with new the data and old templateData.
  4. @Input() templateData on TemplateDirective changes.
    These are ignored because the ChildPushComponent has already detected changes beforehand.

I'm looking for a solution to make the template value visually update at the same time as the other values (i.e. when I run the change detection manually inside ChildPushComponent). Preferably without setTimeout trickery and without removing the OnPush strategy. Any help is much appreciated.

Errors4l
  • 68
  • 5

2 Answers2

2

In your child-push.component.html simply replace this._cd.detectChanges() with this._cd.markForCheck(); that should do it.

For difference between detectChanges() and markForCheck() check out this answer

j4rey
  • 2,582
  • 20
  • 34
  • Thank you very much! Applying markForCheck over detectChanges resolved the issue immediately both in the StackBlitz and in my real project. The linked answer also helped me understand the differences a bit better. -- Is there any problem in calling both functions simultaneously? I'm a bit worried the change could break other logic unrelated to this issue (since the component is quite a beast). – Errors4l Jan 25 '21 at 15:29
  • 1
    You don't need to call both of them, in the StackBlitz example, calling `markForCheck()` resolves the issue. However if for some reason you want to call both of them, *it shouldn't break any logic since it does mutate the data*, instead ensures that the data & front-end are synchronized. Angular `ChangeDetection` are highly optimized, in fact if you don't use the `Push` strategy, `ChangeDetection` are called multiple times, so the question of optimization also rules out. – j4rey Jan 25 '21 at 15:41
1

If I may take the problem in an other point of view,

I would pass data through the input

<app-child-push [data]="data">...</app-child-push>

And then, if you wish to do something when the data changes, then add the ngOnChanges detection directly within the child-push.component.ts.

That way you can keep your changeDetection

There is your starckblitz with the new code : https://stackblitz.com/edit/angular-ivy-g18nwc?file=src/app/child/child-push.component.ts.

I'm not sure I understand your use of the directive here, so let me know if I misunderstood your question and I'll remove my answer.

Sarang
  • 2,143
  • 24
  • 21
Raphaël Balet
  • 6,334
  • 6
  • 41
  • 78
  • Thank you for your answer. I was trying not to do this because with many child components it quickly becomes a lot of (redundant) bindings of the same data. This would definitely be my go to solution if no better solution exists (and arguably it's the better choice because it respects the Angular 'way' of data sharing), but it seems like j4rey's answer does the trick nicely. There's no real reason for using a directive over a component, it's just that in my use case the parent has no template. It is mostly just a container for the child components with some extra logic. – Errors4l Jan 25 '21 at 15:24
  • Thx for the explanation, makes sense. I think it's a way of seeing things ;) I have a question though, but I have the feeling that, with your way of doing this, you'll may finish with a larger amount of code / method cause you're recreating the angular `@Input()` logic with your directive bindings and `setData()` method. Don't you think? – Raphaël Balet Jan 25 '21 at 15:43
  • The code needed to share all the info between the components indeed adds ((needless)) complexity, but this complexity is contained within the component. The problem with changing the structure to multiple input bindings is that this now adds redundant code to hundreds of places where we use the component. So although the code complexity is a bit higher now, there's a lot less boilerplate code present in the code all our devs work with. I agree with your solution and I would probably have gone with it if the other solution wasn't us calling the wrong function in otherwise working code :). – Errors4l Jan 25 '21 at 15:57