owned this note
owned this note
Published
Linked with GitHub
# Design Doc - Component Render Strategies
author: [Michael Hladky]
created: 2020-02-03
status: [draft | in review | approved | **implemented** | obsolete]
doc url: https://hackmd.io/42oAMrzYReizA65AwQ7ZlQ
feedback url: https://github.com/ngrx/platform/issues/2441
design doc template version: 1.0.0
## Objective
<!--
A rough overall explanation of the idea as well as related terms and a quick scatch of the need.
-->
The goal of this document is to implement an efficient way to coalesce renderings of the view in applications.
This is especially of help, in zone-less Angular applications that trigger rendering manually, but also enables us to get zone-less performance in zone-full environments.
> As this document uses specific terminology find detailed explanations in the gist here: [Reactive Angular Terminology](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10).
Coalescing means buffering a set of emissions into a smaller set of those emissions. [Here a general explaination](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#coalescing-of-work).
In the documents manner, it always coalesces all emissions to the last emission of a tick in the event loop scheduled as a microtask.
A more technical explanation of the behavior would be very similar to the already existing [RxJS rate-limiting operators](https://rxjs.dev/api/operators/throttle#see-also).
Here some [diagrams](https://twitter.com/Michael_Hladky/status/1119277221087150081) so "see" the difference in behavior.
Angular itself has implemented several things to enable coalescing.
- Flags for ngZoneEventCoalescing
- scheduleTick called from ApplicationRef.tick()
- scheduleDetectChanges called in elements
Those mostly target rendering. Some general things where this solution can be useful are:
- state derivations
- state updates
- rendering
- event computations
The general goals are:
- to create a coalescing logic that improves the performance of reactive and/or zone-less applications.
- It should be flexible to configure and easy to use.
The technical goal is to create an RxJS operator for a
configurable coalescing strategy:
- acts as a filter/throttle operator for emitted values. The actual work is done with the coalesced result
- enables execution-context scoping (animationFrame, Promise, setTimeout)
- enables usage-context scoping (window, Class, component instance, LEmbeddedView)
The real challenge here is to find the right usage-context for scoping.
Here an [collection/explaintion of thoughts](https://docs.google.com/presentation/d/1QBx3nTyDpQvmNnN5NNV6apJO70rSN6lxCKS-OmPPPA8/edit#slide=id.g6e7b315996_0_226) and some edge cases.
### Expected User/Developer Experience
<!--
Describe what will the user be able to do and how will they be able to do it thanks to this new feature. This should be done in a story format, described from the user’s/developer’s perspective, with as many details as possible but without going into the implementation details.
-->
The developer of a reactive application knows exactly when to rerener.
Currently Angular takes over this decision by relying on the zone mechanism only.
With this feature the developer would be able to use pipes, directives in the template or operators in the class to triger rendering.
With the provided strategies the developers are able to specify how they want to render and which performance optimisations should br used.
As there is quite a bit knowledge needed to use the right combintions the strategies are designed to provide an intuitively names set of strategies that ease this decision and take away the in-depth understanding.
This enables developer to get zone-less performance even within zonefull applications.
### Background
<!--
Stuff one needs to know to understand this doc: motivating examples, previous versions, and problems, links to related projects/design docs, etc. You should mention related work if applicable. This is background; do not write about ideas to solve problems here.
-->
Relevant material to read first:
- [Angular ComponentView and EmbeddedView in Ivy ](https://hackmd.io/X1wy5Y1TSIWibaHiaAHyBA?view) In: Hackmd, Michael Hladky.
### Prior Art
To implement the described feature 3 sections need to be understood:
- Change detection in Angular
- Render methods of Angular
- The coalescing mechanism in general
- Coalescing and Render Methods in Agnular
#### Change Detection in Angular
- Default,
- OnPush, dirty flagging
- Zone,
#### Render Methods and Behaviour
This information is needed to get an overview of Angulars render methods and it'S differences.
The tables describe the following:
- Render Method: Name and location of the methods responsible for rendering
- Renders: Position in the component tree from where it will render.
The app means from the root of the tree, component means from the component on which the method is called downwards.
- ZoneLess: If a zone environment is needed. More specifically is the token `ngZone` is derived from `NgZone` or `NgNoopZone`.
- Coalesce Scope: Angular v8 and v9 both have variations where coalescing is present and scoped in ApplicationRef
- Execution: Tells how the method is executed. e.g. microtask
**>=ViewEngine**
| Render Method | Renders |
| ------------------ | -------- |
| cdRef.markForCheck | App |
| markForCheck+tick | App |
| cdRef.detectChange | Children |
**Ivy**
| Render Method | Renders |
| ------------------ | -------- |
| ɵmarkDirty | App |
| ɵdetectChange | Children |
An interesting thing is `ChangeDetectorRef#markForCheck`.
Internally it runs `ApplicationRef#tick` to render the app. It is triggered by logic related to `ngZone`.
This menas dirty flagging of a compoent and rendering can be used independent from each other in `NgNoopZone` environments.
#### The Coalescing Mechanism in General
To understand this section we split it into 2 parts.
- Coalescing in general
- Scoped Coalescing
**General coalescing**
The basic idea of coalescing and also the current implementation in Angular holds a global flag that determines if synchrounous code is running and schedules a micro or macro task that runs some logic related to rendering after all the sync cond is executed.
Primitive implementation:
```typescript
export function getCoalesceFn() {
const isCoalescing = false;
return (work: () => void) => {
// If work is already scheduled to do nothing
if (isCoalescing) {
return;
}
// If NO work is scheduled
// request a new animationFrame and set isCoalescing to a truthy value
isCoalescing = executionContext(() => {
// Reset requestAnimationFrameId
isCoalescing = false;
// Logic here will get buffered in the micro task queue and executed only ones
work();
});
}
}
// Example:
const doCoalesce = getCoalesceFn();
doCoalesce(() => console.log(('1'));
doCoalesce(() => console.log(('2'));
doCoalesce(() => console.log(('3'));
doCoalesce(() => console.log(('4'));
// 4
```
**Scoping**
If we think a step further, we can see that this implementation includes a sort of scope in which the coalescing is active.
In the abofe implementation it was global. e.g. scoped by the window object.
On a component level the will lead to situations where only the first compoent will get rerendered, others not as the scoping does not know aboul components.
This leads to the need of a local scope. coalescing should be possible on component level.
To enable that we need to be able to share a scope tied to the components view.
A promitive solution of a shared scope can look like that:
```typescript
export interface CoalesceConfig {
context?: {isCoalescing: boolean};
}
export function getCoalesceFn(cfg: CoalesceConfig = {isCoalescing: boolean} ) {
return (work: () => void) => {
if (cfg.isCoalescing) {
return;
}
cfg.isCoalescing = executionContext(() => {
cfg.isCoalescing = false;
work();
});
}
}
// Example:
const scope1 = {isCoalescing: false};
const doCoalesce1 = getCoalesceFn(scope1);
const scope2 = {isCoalescing: false};
const doCoalesce2 = getCoalesceFn(scope2);
doCoalesce1(() => console.log(('1'));
doCoalesce1(() => console.log(('2'));
doCoalesce2(() => console.log(('3'));
doCoalesce2(() => console.log(('4'));
// 2, 4
```
As this lib targets mainly reactive code we would need to put this logic into an RxJS operator. To get a better idea I put some psudo code of how it could look.
RxJS Operator Version:
```typescript
// Basic behaviour
from([1,2,3,4]).pipe(
coalesce(cfg),
).subscribe(console.log);
// 4
// Real-life usage
stateChanges$.pipe(
tap(v => updateModel(v)),
coalesce(cfg),
tap(_ => renderView())
).subscribe();
```
### Coalescing and Render Methods in Agnular
The tables describe the following:
- Render Method: Name and location of the methods responsible for rendering
- Renders: Position in the component tree from where it will render.
The app means from the root of the tree, component means from the component on which the method is called downwards.
- ZoneLess: If a zone environment is needed. More specifically is the token `ngZone` is derived from `NgZone` or `NgNoopZone`.
- Coalesce Scope: Angular v8 and v9 both have variations where coalescing is present and scoped in ApplicationRef
- Execution: Tells how the method is executed. e.g. microtask
**>=ViewEngine**
| Render Method | Renders | Zone Type | Coalesce Scope | Execution |
| ------------------ | -------- | ---------- | -------------- | ---------- |
| cdRef.markForCheck | App | NgZone | ApplicationRef | Micro Taks |
| markForCheck+tick | App | NgNoopZone | ApplicationRef | Micro Taks |
| cdRef.detectChange | Children | NgNoopZone | None | Sync Taks |
**Ivy**
| Render Method | Renders | Zone Type | Coalesce Scope | Execution |
| ------------------ | -------- | ---------- | -------------- | -----------|
| ɵmarkDirty | App | NgNoopZone | ApplicationRef | Micro Taks |
| ɵdetectChange | Children | NgNoopZone | None | Sync Taks |
Especially interesting are the columns ZoneLess and Execution.
Zone Type is relevant for migrations to a zone-less environment.
Execution is very important in terms of custom coalescing strategies,
as the could influence each other badly if both are used together.
Performance Features and Incompatibilities
Due to some incompatibilities or the combinations are just useless not all Angular built in features can be combined with the ngrx/component performace features.
**Working Combinations:**
- \>=ViewEngine
| | Render Method | Coalescing | Coalesce Scope |
| -------- | -------------------| ---------- | -------------- |
| ZoneFull | cdRef.markForCheck | ❌ | None |
| ZoneLess | markForCheck+tick | ❌ | None |
| ZoneLess | cdRef.detectChange | ❌ | None |
| ZoneLess | cdRef.detectChange | ✔️ | None |
| ZoneLess | cdRef.detectChange | ✔️ | Compoennt |
- Ivy
| | Render Method | Coalescing | Coalesce Scope |
| -------- | -------------------| ---------- | -------------- |
| ZoneFull | ɵmarkDirty | ❌ | None |
| ZoneLess | ɵmarkDirty | ❌ | None |
| ZoneLess | ɵdetectChange | ✔️ | None |
| ZoneLess | ɵdetectChange | ✔️ | LView |
## Strategy Design
### Cases to Consider
- **Push Pipe**
- 01. One single-shot observable bound by one ngrxPush as template expression
```htmlmixed
{{sync1$ | ngrxPush}}
```
- 02. One single-shot observable bound by multiple ngrxPush as template expression
```htmlmixed
{{sync1$ | ngrxPush}}
{{sync1$ | ngrxPush}}
{{sync1$ | ngrxPush}}
```
- 03. Multiple single-shot observables bound by multiple ngrxPush as template expression
```htmlmixed
{{sync1$ | ngrxPush}}
{{sync2$ | ngrxPush}}
{{sync3$ | ngrxPush}}
```
- 04. One sync multi-shot observables bound by multiple ngrxPush as template expression
```htmlmixed
{{syncArray1$ | ngrxPush}}
```
- 05. One sync multi-shot observables bound by multiple ngrxPush as input binding
```htmlmixed
<child1 [value]="sync1$ | ngrxPush"></child>
```
<app-push-child05 [value]="value1$ | ngrxPush: cfg"></app-push-child05>
- 11. One single-shot observable bound by one ngrxPush as input binding
```htmlmixed
<child1 [value]="sync1$ | ngrxPush"></child>
```
- 12. One single-shot observable passed directly to input binding rendered over ngrxPush
```htmlmixed
<child [value]="sync1$"></child>
```
- 13. One single-shot observable bound by multiple ngrxPush as input binding
```htmlmixed
<child [value]="sync1$ | ngrxPush"></child>
<child [value]="sync1$ | ngrxPush"></child>
<child [value]="sync1$ | ngrxPush"></child>
```
- 14. Multiple single-shot observables bound by multiple ngrxPush as input binding
```htmlmixed
<child [value]="sync1$ | ngrxPush"></child>
<child [value]="sync2$ | ngrxPush"></child>
<child [value]="sync3$ | ngrxPush"></child>
```
- 21. One single-shot observable bound by one ngrxPush as input binding. The nested components uses ngrxPush to render changes.
```htmlmixed
<child [value]="sync1$ | ngrxPush">
<!-- Internal Template Start -->
{{sync1$ | ngrxPush}}
<!-- Internal Template End -->
</child>
```
- **Let Directive**
- 01. One single-shot observable bound by one ngrxLet as input binding with as syntax
```htmlmixed
<ng-container *ngrxLet="sync1$ as sync1">{{sync1}}</ng-container>
```
- 02. One single-shot observables bound by multiple ngrxLet as input binding with as syntax
```htmlmixed
<ng-container *ngrxLet="sync1$ as sync1">{{sync1}}</ng-container>
<ng-container *ngrxLet="sync1$ as sync1">{{sync1}}</ng-container>
<ng-container *ngrxLet="sync1$ as sync1">{{sync1}}</ng-container>
```
- 03. Multiple single-shot observables bound by multiple ngrxLet as input binding with as syntax
```htmlmixed
<ng-container *ngrxLet="sync1$ as sync1">{{sync1}}</ng-container>
<ng-container *ngrxLet="sync2$ as sync2">{{sync2}}</ng-container>
<ng-container *ngrxLet="sync3$ as sync3">{{sync3}}</ng-container>
```
- 11. One single-shot observable bound by one ngrxLet as input binding with let syntax
```htmlmixed
<ng-container *ngrxLet="sync1$; let sync1">{{sync1}}</ng-container>
```
- 12. One single-shot observables bound by multiple ngrxLet as input binding with let syntax
```htmlmixed
<ng-container *ngrxLet="sync1$; let sync1">{{sync1}}</ng-container>
<ng-container *ngrxLet="sync1$; let sync1">{{sync1}}</ng-container>
<ng-container *ngrxLet="sync1$; let sync1">{{sync1}}</ng-container>
```
- **TemplateRef injection**
@TODO consider more edge cases
- 01. @TODO inters text https://ng-run.com/edit/vbcTXFp9cVQtsigapDs2?open=app%2Fapp.component.html&aot=true
```htmlmixed
<insertion [template]="ref">
<!-- Internal Template Start -->
<ng-container [ngTemplateOutlet]="template"> </ng-container>
<!-- Internal Template End -->
</insertion>
<ng-template #ref>
<span>{{obs$ | push}}</span>
</ng-template>
```
- **Push Pipe and Let Directive Mixed Setup**
@TODO consider more edge cases
- 01. One single-shot observable bound by one ngrxPush and one ngrxLet as input binding
```htmlmixed
<ng-container *ngrxLet="sync1$ as sync1">{{sync1}}</ng-container>
<child [value]="sync1$ | ngrxPush"></child>
```
- 02. Kitchensink :D
```htmlmixed
{{nums1$ | ngrxPush}}
<child [value]="nums1$ | ngrxPush"></child>
<p *ngFor="let num of nums1$ | ngrxPush">{{num}}</p>
<ng-container *ngIf="nums1$ | ngrxPush as sync1">{{sync1 | json}}</ng-container>
<ng-container *ngrxLet="nums1$ as sync1">{{sync1 | json}}</ng-container>
```
- **Push Pipe and Let Directive and Class Based Change Detection**
@TODO consider more edge cases
- 01. One single-shot observable bound by one ngrxPush and one ngrxLet as input binding and in the component there is CD
```typescript
sync1$.pipe(
tap(() => detetChange(this))
)
.subscribe();
```
```htmlmixed
<ng-container *ngrxLet="sync1$ as sync1">{{sync1}}</ng-container>
<child [value]="sync1$ | ngrxPush"></child>
```
### Strategies
A strategy is considered as a composition of render methods of the used engine and the additional performance features.
Factors impacting the performance are:
- the render method
- the coalescing feature
- the coalesce scope
![](https://i.imgur.com/qYvDXJV.png)
**@TODO this section needs to check-in form of code and working examples**
Another relevant information is that Angular's coalescing features
- markForCheck => @TODO name the exact method and execution context
- markDirty => @TODO name the exact method and execution context
are compatible with the new coalescing feature. More specific to the actual duratonSelector (the unpatched animationFrame).
The strategy names are related to their impact and also serve as a roadmap like listing.
**What about:**
- VE/I - Options for ViewEngine / Ivy
- mFC - `cdRef.markForCheck`
- dC - `cdRef.detectChanges`
- ɵMD - `ɵmarkDirty`
- ɵDC - `ɵdetectChanges`
- LV - `LView`
- C - `Component`
| Name | ZoneLess VE/I | Render Method VE/I | Coalescing VE/I |
|-------------| --------------- | ------------------------ | ----------------- |
| `asyncLike` | ❌/❌ | mFC / mFC | ❌ |
| `global` | ❌/✔️ | mFC / ɵMD | ❌ |
| `local` | ✔️/✔️ | dC / ɵDC | ✔️ + C/ LV |
| `noop` | ❌/❌ | no rendering | ❌ |
**asyncLike**
This strategy is the drop-in replacement for Angular's built-in `async` pipe. This is the only strategy that **does not also work in zone-less environments**.
**global**
**local**
**noop**
This strategy does nothing but asignin the values to the view context. Handy for debugging.
## Prototype
Scoping by LView:
![](https://i.imgur.com/YZgyMR4.png)
![](https://i.imgur.com/LhW2ZiE.png)
![](https://i.imgur.com/JL97WIF.png)
### Minimal Design
<!--
Include only the essential parts of the code. No error handling, no performance consideration.
This section should help to understand the essential implementation in one small piece of code
-->
Created a configurable RxJS operator to coalesce the emissions.
Roughly speaking, this operator should act as a rate-limiting operator on the event-loop level.
Buffer all synchronous emitted values and send the last of them as the first microtask.
**Usage in the Push Pipe**
```typescript
// Implementation
@Pipe({ name: 'ngrxPush', pure: false })
export class PushPipe implements PipeTransform {
private renderedValue?: unknown;
private subscription: Subscription = new Subscription();
constructor(protected cdRef: ChangeDetectorRef) {
}
transform<T>(obs: Observable<T> | Promise<T> | null | undefined): T | null {
// Use unpatched verson of animation frame
const durationSelector = () => generateFrames(
(window as any).__zone_symbol__requestAnimationFrame,
(window as any).__zone_symbol__cancelAnimationFrame
);
const config = {context: this.cdRef['_lView'] as any};
this.subscription.add(obs.pipe(
coalesce(durationSelector, config),
tap({
next(valueToRender: unknown) {
this.renderedValue = valueToRender;
this.cdRef.detectChanges();
}
})
)
.subscribe());
return this.renderedValue;
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
```
```htmlmixed
<!-- Real-life usage -->
<!-- Coalescing Context Off -->
Renders component 3 times:
{{value | ngrxPush}} {{value | ngrxPush}} {{value | ngrxPush}}
<!-- Coalescing Context On -->
Renders component 1 time:
{{value | ngrxPush}} {{value | ngrxPush}} {{value | ngrxPush}}
```
### Detailed Design
<!--
Include some important cases. Error handling, and consider how to deal with e.g. performance.
This section should help to understand the tricky implementations in more detail.
Don't take too much attention to typing if it bloats the code too much.
-->
```typescript
export interface CoalescingContext {
isCoalescing: any | undefined;
}
export interface CoalesceConfig {
context?: CoalescingContext;
leading?: boolean;
trailing?: boolean;
}
const cfg: CoalesceConfig = {
context: LComponentView;
executionContextRef: (window as unknown).__zone_symbol__requestAnimationFrame;
};
// @TODO put suggestion for the strategy pattern
```
### Caveats
<!--
You may need to describe what you did not do or why simpler approaches don't work. Mention other things to watch out for (if any).
Security Considerations
How you’ll be secure. Considerations should include sanitization, escaping, existing security protocols and web standards, permission, user privacy, etc.
None
-->
**Coalescing context in viewing**
The actual strategy for coalescing within a context is only working in ivy.
**As any context object can be passed and will be patched**
We have to apply the flag `isCoalescing` over a symbol to the context avoid conflicts with other present properties.
**Used inside a zone.js patched environment**
```typescript
// Unpatched requestAnimationFrame reference
window.__zone_symbol__requestAnimationFrame;
```
### Performance Considerations
<!--
Try to describe under which conditions the suggested solution is performant and which factors play the key role when starting to get bad performance.
Describe a specific situation in which we can run into performance problems
If possible provide a POC or a theoretical explanation of a possible solution.
-->
As this design doc is focusing only on performance this section is not applicable.
## Documentation Plan
<!--
Try to describe the important parts of the implementation and how to documented it e.g. importance, a priority by relevance for user, level of detail, example needed.
-->
As the scope of this part of the package is quite small I consider the documentation in text and some examples in the demo app and as code snippets
is everything we need.
Following operators needed to get documented:
- generateFrames
- coalesce
- detectChanges
- markDirty
- markForCheck
### Style Guide Impact
<!--
Does the documentation influence the way the current style guide is structured?
Also, does the new documentation introduces any technical implementations of the docs?
If so please name them and give a detailed description of the impact and if possible some POCs.
-->
The needed documentation has no impact on the current way the documentation is maintained.
## Developer Impact
### Public API Surface Impact
<!--
Are you adding new public APIs? If so, what's the change? (method signature changes, new classes/interfaces, new npm packages, config file properties or new config files altogether)
Are the new APIs consistent with the existing APIs? Do they follow naming conventions and use correct terminology?
-->
The new behavioral RxJS operator does not affect the existing API interface. It will get introduced under a new package `@ngrx/component`.
It exposes only the operator and needed interfaces and types.
Following dependencies are needed:
- @rxjs@>=6.5.4
The new operator is named `coalesce`.
It is used the same way as any other RxJS operator is used `o$.pipe(coalesce(cfg))`.
The term 'coalesce' is used to refer to its direct meaning. 'Notifications' refers to [RxJS Notification](https://github.com/ReactiveX/rxjs/blob/388c4852948660abcff22d7b82ccb0a29c77428c/src/internal/Notification.ts#L31) and 'ExecutionContext' refers to the task stacks in the event loop (sync, micro, macro)
This tries to give the user an intuitive understanding of its behavior out of the operators' name itself.
### Developer Experience Impact
<!--
How will this change impact developer experience?
Are we adding new tools developers need to learn? Are we asking developers to change their workflows in any way?
Are we placing any new constraints on how the developer should write the code to be compatible with this change?
-->
This has no impact on a user's workflow but can increase performance and enables them to run zone-less.
If the users decide to disable zone he may need to get more familiar with RxJS operators.
Other than this the user does not need to adapt or change anything.
In addition to that, the created operators can be used anywhere else too, to solve related problems.
### Breaking Changes
The new pipe will not introduce any breaking changes.
The new behavior can be implemented in the underlying `cdAwaer` class and no public API needs to be touched.
The configuration is done over pipe parameters or globally over some tokens.
And will get introduced wich the above adoption.
### Rollout Plan
<!--
Are the implementation delays about to negatively affect or delay the release of other features or increase the size.
-->
The operator can be rolled out in the following way:
**Alpha releases of the operator** include only animationFrame as coalescing frame and scoping by a context.
Shipped Features:
- scope coalescing by context object `CoalesceConfig.context`
- define coalescing by executin context `CoalesceConfig.executionContextRef`
- control the emission of leading values `CoalesceConfig.leading`
- control the emission of trailing values `CoalesceConfig.trailing`
**Beta releases of the @ngrx/component package** parts of the package include a configuration argument to opt-in the operator and its mechanism.
**Rc and official releases of the @ngrx/component package** parts of the package have the feature opt-out by default.
Alos internal interfaces etc can be exposed in this releases.
### Rollback Plan
<!--
How do you plan to roll back the change if major issues are found?
-->
In the late alpha versions, we introduce the operator.
If any critical problems occur in the alpha version we can just don't ship it further versions.
Until the first beta, we should have to clarify this. This is the moment we introduce the configuration option for the part of the package that uses this operator.
The feature can be rolled back by removing the option from the config object.
If a major issue is found in RC or even official release we can remove the parts of the package from the package and disable the related config options.
There are no implementation details that could delay the release of other packages. Also, the bundle size of other features will not increase by introducing this feature.
### Maintenance Plan
<!--
Explain how this library will be maintained going forward in releases after the initial release.
This includes not only releases of the subject of this document but also respects its dependencies.
-->
The code relies on some RxJS operators naming: `Observable, filter, tap`
`tap` may break in the future if Observabls make it in the standard the most probably will have callbacks as arguments instead of an observer object.
But this is also a minor refactoring.
## Alternatives considered
<!--
Include alternate design ideas you tried out, but didn't continue with them.
List their disadvantages or at least why you did not invest more time in researching them.
-->
**Pipe:**
- context: PipeClass, executionContextRef: animationFrame
Fails if multiple components are nested @TODO double check
**Directive:**
- context: DirectiveClass, executionContextRef: animationFrame
Fails if multiple `*ngrxLet` directives are in the same template context
## Work Breakdown
<!--
Explain how multiple people would actively working on the suggested code base.
If needed include branching suggestions or the way code interacts
-->
As the pipes scope is small enough to get maintained by one person at a time we don't need to describe the breakdown of work.
## Resources
**Research Paper/Design Docs/StackBlitz/Video/Podcast/Blog/Tweet/Graphic:**
- [Angular9 ivy change-detection preview](https://alexzuza.github.io/angular-9-ivy-change-detection-preview/)
- [Examples of the coalescing mechanism and its behavior](https://stackblitz.com/edit/rxjs-coalescing-of-work?file=index.html) In: StackBlitz, Michael Hladky. Hladky.
- [Coalescing of Change Detection](https://docs.google.com/presentation/d/1QBx3nTyDpQvmNnN5NNV6apJO70rSN6lxCKS-OmPPPA8/edit?usp=sharing) In: Google Slides, Michael Hladky.
- [Reduce Change Detection Cycles with Event Coalescing in Angular](https://netbasal.com/reduce-change-detection-cycles-with-event-coalescing-in-angular-c4037199859f) In: Medium, Netanel Basal, 2019-12-10.
- [How can I change the viewContainerRef of an ng-template](https://stackoverflow.com/questions/60117068/how-can-i-change-the-viewcontainerref-of-a-ng-template/60126090#60126090) In: StackOverflow
- [Ivy Minor Changes](https://docs.google.com/document/d/1Dije0AsJ0PxL3NaeNPxpYDeapj30b_QC0xfeIvIIzgg/preview) In: Google Docs, Angular.
**Github Pull Request/Issue/Doc/Source Link:**
- [Angular change detection spec](https://github.com/angular/angular/blob/9bd959076730c4e22ceadda73694198b4f01b9e0/packages/core/test/acceptance/change_detection_spec.ts)
- [LComponentView vs LEmbeddedView](https://github.com/ngrx/platform/pull/2046#issuecomment-581167650)
- [getNativeRequestAnimationFrame](https://github.com/angular/angular/blob/44623a116158c8048ce61a873ed63a290c039202/packages/core/src/util/raf.ts)
- [add a flag in bootstrap to enable coalesce event to improve performance](https://github.com/angular/angular/pull/30533/files#diff-9d549aa403494fd043a146b8db64f503R264-R275)
- [RFC: Component: Evaluation of using angular's internal `ɵdetectChanges` method](https://github.com/ngrx/platform/issues/2050)
- [isAngularZone](https://github.com/angular/angular/blob/8.2.14/packages/core/src/zone/ng_zone.ts#L143-L144)
- [coalesce change detection](https://github.com/angular/angular/blob/master/packages/core/src/zone/ng_zone.ts#L264)
- [Angulars scheduleDetectChanges](https://github.com/angular/angular/blob/master/packages/elements/src/component-factory-strategy.ts#L202-L215)
- [CHANGELOG v9](https://github.com/angular/angular/blob/master/CHANGELOG.md#breaking-changes)