componentReactive primitives for components
component This RFC proposes a nice reactive integration of components/directives in Angular.
It's main goal is to provide a set of primitives that serve as the glue between custom reactive code and the framework.
Parts of Angular like the ReactiveFormsModule, RouterModule, HttpClientModule etc. are already reactive.
And especially when composing them together we see the benefit of observables. i.e. http composed with router params.
For those who prefer imperative code, it's little effort to restrict it to a single subscription.
On the other hand for those who prefer reactive code, it's not that easy.
A lot of conveniences is missing, and beside the async pipe there is pretty much nothing there to take away the manual mapping to observables.
Furthermore, an increasing number of packages start to be fully observable based.
A very popular and widely used example is ngRx/store. It enables us to maintain global push-based state management based on observables.
Also, other well-known libraries, angular material provide a reactive way of usage.
This creates even more interest and for so-called reactive primitives for the Angular framework, like the async and other template syntax, decorators and services.
The first step would be to give an overview of the needs and a suggested a set of extensions to make it more convenient to work in a reactive architecture.
In the second step, We will show the best usage and common problems in a fully reactive architecture.
This proposal
give an overview of the needs
suggests a set of extensions to make it more convenient to work reactive with angular components
As the main requirement for a reactive architecture in current component-oriented
frameworks are handling properties and events of components as well as several specifics for
rendering and composition of observables.
In angular, we have an equivalent to properties and events, _input_ and _output_ bindings_.
But we also have several other options available to interact with components.
The goal is to list all features in angular that need a better integration.
We cover an imperative as well as a reactive approach for each option.
We consider the following decorators:
And consider the following bindings:
Inside of a component or directive we can connect properties with the components in it bindings over the @Input() decorator.
This enables us to access the values of the incoming in the component.
_Receive property values over @Input('state')_
Imperative approach:
@Component({
selector: 'app-child',
template: `<p>State: {{state | json}}</p>`
})
export class ChildComponent {
@Input() state;
}
Reactive approach:
Here we have to consider to cache the latest value from state-input binding.
As changes fires before AfterViewInit, we normally would lose the first value sent. Using some caching mechanism prevents this.
Furthermore and most importantly this makes it independent from the lifecycle hooks.
@Component({
selector: 'app-child',
template: `<p>State: {{state$ | async | json}}</p>`
})
export class ChildComponent {
state$ = new ReplaySubject(1);
@Input()
set state(v) {
this.state$.next(v);
};
}
Needs:
Some decorator that automates the boilerplate of settings up the subject and connection it with the property.
Here ReplaySubject is critical because of the life cycle hooks.
@Input is fired first on OnChange where the first moment where the view is ready would be AfterViewInit
Boilerplate Automation
For every binding following steps could be automated:
- setting up a
Subject- hooking into the
setterof the input binding and.next()the incoming value
Early Producer
All input bindings are so-called "early producer". A cache mechanism is needed as followed:
- Use a
ReplaySubjectwithbufferSizeof1to emit notifications
_Send event over eventEmitter.emit(42)_
Inside of a component or directive, we can connect events with the components output bindings over the @Output() decorator.
This enables us to emit values to its parent component.
Imperative approach:
@Component({
selector: 'app-child',
template: `<button (click)="onClick($event)">Btn</button>`
})
export class ChildComponent {
@Output()
clickEmitter = new EventEmitter();
onClick(e) {
this.clickEmitter.next(e.timeStamp);
}
}
Reactive approach:
Here we change 2 things.
We use a Subject to retrieve the button click event and
provide an observable instead of an EventEmitter for @Output().
@Component({
selector: 'app-child',
template: `<button (click)="clickEmitter.next($event)">Btn</button>`
})
export class ChildComponent {
btnClick = new Subject();
@Output()
clickEmitter = this.btnClick
.pipe(
map(e => e.timeStamp)
);
}
Needs:
No need for an extension.
No need for custom extensions
Due to the fact that we can also provide anObservableasEventEmittersthere is no need for as extension
---
_Receive event from the host over @HostListener('click', ['$event'])_
Inside of a component or directive, we can connect host events with a component method over the @HostListener() decorator.
This enables us to retrieve the host's events.
Imperative approach:
@Component({
selector: 'app-child',
template: `<p>Num: {{num}}</p>`
})
export class ChildComponent {
num = 0;
@HostListener('click', ['$event'])
onClick(e) {
this.num = ++this.num;
}
}
Reactive approach:
@Component({
selector: 'app-child',
template: `<p>Num: {{num$ | async}}</p>`
})
export class ChildComponent {
numSubj = new Subject();
num$ = this.numSubj.pipe(scan(a => ++a));
@HostListener('click', ['$event'])
onCllick(e) {
this.numSubj.next(e);
}
}
Needs:
We would need a decorator automates the boilerplate of the Subject creation and connect it with the property.
As subscriptions can occur earlier than the Host could send a value we speak about "early subscribers".
This problem can be solved as the subject is created in with instance construction.
Boilerplate Automation
For every binding following steps could be automated:
- setting up a
Subject- hooking into the
setterof the input binding and.next()the incoming value
Early Producer
Make sure the createdSubjectit present early enough
---
_Receive property changes from the host over @HostBinding('class')_
Inside of a component or directive, we can connect the DOM attribute as from the host with the component property.
Angular automatically updates the host element over change detection.
In this way, we can retrieve the host's properties changes.
Imperative approach:
@Component({
selector: 'app-child',
template: `<p>color: {{className}}</p>`,
})
export class ChildComponent {
className = 'visible';
@HostBinding('class')
get background() {
return this.className;
}
}
Reactive approach:
TBD
Needs:
Provide an observable instead of a function.
Here again, we would need a decorator that automates the Subject creation and connection.
As subscriptions can occur earlier than the Host could be ready we speak about "early subscribers".
This problem can be solved as the subject is created in with instance construction.
Boilerplate Automation
For every binding following steps could be automated:
- setting up a
Subject- hooking into the
setterof the input binding and.next()the incoming value
Early Subscribers
Make sure the createdSubjectit present early enough
---
_Send value changes to child compoent input [state]="state"_
In the parent component, we can connect component properties to the child
component inputs over specific template syntax, the square brackets [state].
Angular automatically updates the child component over change detection.
In this way, we can send component properties changes.
Imperative approach:
@Component({
selector: 'my-app',
template: `
<app-child [state]="state"></app-child>
`
})
export class AppComponent {
state = 42;
}
Reactive approach:
Important to say is that with this case we can ignore the life cycle hooks as the subscription happens always right in time.
We cal rely on trust that subscription to state$ happens after AfterViewInit.
Inconsistent handling of undefined variables
It is important to mention the inconsistent handling of undefined variables and observables that didn't send a value yet.
@Component({
selector: 'my-app',
template: `
<app-child [state]="state$ | async"></app-child>
`
})
export class AppComponent {
state$ = of(42);
}
Needs:
As we know exactly when changes happen we can trigger change detection manually. Knowing the advantages of subscriptions over the template and lifecycle hooks the solution should be similar to async pipe.
NgZone could be detached
As all changes can get detected we could detach the pipe from theChangeDetectionand trigger it on every value change
Performance optimisations
- consider scheduling over
AnimationFrameSchedulerthe output is always for the view
Implement strict and consistent handling of undefined for pipes
A pipe similar toasyncthat should act as follows:
- when initially passed
undefinedthe pipe should forwardundefinedas value as on value ever was emitted- when initially passed
nullthe pipe should forwardnullas value as on value ever was emitted- when initially passed
of(undefined)the pipe should forwardundefinedas value asundefinedwas emitted- when initially passed
of(null)the pipe should forwardnullas value asnullwas emitted- when initially passed
EMPTYthe pipe should forwardundefinedas value as on value ever was emitted- when initially passed
NEVERthe pipe should forwardundefinedas value as on value ever was emitted- when reassigned a new
Observablethe pipe should forwardundefinedas value as no value was emitted from the new- when completed the pipe should keep the last value in the view until reassigned another observable
- when sending a value the pipe should forward the value without changing it
Already existing similar packages:
In the following, we try to explore the different needs when working with observables in the view.
Lets examen different situations when binding observables to the view and see how the template syntax that Angular already provides solves this. Let's start with a simple example.
Multiple usages of async pipe
Here we have to use the async pipe twice. This leads to a polluted template and introduces another problem with subscriptions.
As observables are mostly unicasted we would receive 2 different values, one for each subscription.
This pushes more complexity into the component code because we have to make sure the observable is multicasted.
@Component({
selector: 'my-app',
template: `
{{random$ | async}}
<comp-b [value]="random$ | async">
</comp-b>
`})
export class AppComponent {
random$ = interval(1000)
.pipe(
map(_ => Math.random()),
// needed to be multicasted
share()
);
}
Binding over the as syntax
To avoid such scenarios we could use the as syntax to bind the observable
to a variable and use this variable multiple times instead of using the async pipe multiple times.
@Component({
selector: 'my-app',
template: `
<ng-container *ngIf="random$ | async as random">
{{random}}
<comp-b [value]="random">
</comp-b>
</ng-container>
`})
export class AppComponent {
random$ = interval(1000)
.pipe(
map(_ => Math.random())
);
}
Binding over the let syntax
Another way to avoid multiple usages of the async pipe is the let syntax to bind the observable to a variable.
@Component({
selector: 'my-app',
template: `
<ng-container *ngIf="random$ | async; let random = ngIf">
{{random}}
<comp-b [value]="random">
</comp-b>
</ng-container>
`})
export class AppComponent {
random$ = interval(1000)
.pipe(
map(_ => Math.random())
);
}
Both ways misuse the *ngIf directive to introduce a context variable and not to display or hide a part of the template.
This comes with several downsides:
*ngIf directive *ngIf directive is triggered be falsy values, but we don't want to conditionally show or hiding content,async pipe *ngIf directive triggered by falsy values
@Component({
selector: 'my-app',
template: `
<ng-container *ngIf="random$ | async as random">
{{random}}
<comp-b [value]="random">
</comp-b>
</ng-container>
`})
export class AppComponent {
random$ = interval(1000)
.pipe(
map(_ => Math.random() > 0.5 ? 1 : 0)
);
}
As we can see, in this example the ng-container would only be visible if the value is 1 and therefore truthy.
All falsy values like 0 would be hidden. This is a problem in some situations.
We could try to use *ngFor to avoid this.
Context variable over the *ngFor directive
@Component({
selector: 'my-app',
template: `
<ng-container *ngFor="let random of [random$ | async]">
{{random}}
<comp-b [value]="random">
</comp-b>
</ng-container>
`})
export class AppComponent {
random$ = interval(1000)
.pipe(
map(_ => Math.random() > 0.5 ? 1 : 0)
);
}
By using *ngFor to create a context variable we avoid the problem with *ngIf and falsy values.
But we still misuse a directive. Additionally *ngFor is less performant than *ngIf.
Nested ng-container problem
@Component({
selector: 'my-app',
template: `
<ng-container *ngIf="observable1$ | async as color">
<ng-container *ngIf="observable2$ | async as shape">
<ng-container *ngIf="observable3$ | async as name">
{{color}}-{{shape}}-{{name}}
<app-color [color]="color" [shape]="shape" [name]="name">
</app-color>
</ng-container>
<ng-container>
</ng-container>
`})
export class AppComponent {
observable1$ = interval(1000);
observable2$ = interval(1500);
observable3$ = interval(2000);
}
Here we nest ng-container which is a useless template code.
A solution could be to compose an object out of the individual observables.
This can be done in the view or the component.
Composing Object in the View
@Component({
selector: 'my-app',
template: `
<ng-container
*ngIf="{
color: observable1$ | async,
shape: observable2$ | async,
name: observable3$ | async
} as c">
{{color}}-{{shape}}-{{name}}
<app-other-thing [color]="c.color" [shape]="c.shape" [name]="c.name">
</app-other-thing>
</ng-container>
`})
export class AppComponent {
observable1$ = interval(1000);
observable2$ = interval(1500);
observable3$ = interval(2000);
}
Here we can use *ngIf again because and object is always truthy. However, the downside here is
we have to use the async pipe for each observable. `Furthermore we have less control over the single observables.
A better way would be to move the composition into the template and only export final compositions to the template.
Composition in the Component
@Component({
selector: 'my-app',
template: `
<ng-container *ngIf="composition$ | async as c">
{{color}}-{{shape}}-{{name}}
<app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
</app-color>
</ng-container>
`})
export class AppComponent {
observable1$ = interval(1000);
observable2$ = interval(1500);
observable3$ = interval(2000);
composition$ = combineLatest(
this.observable1$.pipe(startWith(null), distinctUntilChanged()),
this.observable2$.pipe(startWith(null), distinctUntilChanged()),
this.observable3$.pipe(startWith(null), distinctUntilChanged()),
(color, shape, name) => ({color, shape, name})
)
.pipe(
share()
);
}
As we see in this example in the component we have full control over the composition.
Needs:
We need a directive that just defines a context variable without any interaction of the actual dom structure.
The syntax should be simple and short like the as syntax. It should take over basic performance optimizations.
Also, the consistent handling of null and undefined should be handled.
Implement more convenient binding syntax
To improve usability we should fulfill the following:
- the context should be always present.
*ngIf="{}"would do that already- avoid multiple usages of the `async pipe
- move subscription handling in the directive
- better control over the context. Maybe we could get rid of the
asas variable??- implement an internal layer to handle null vs undefined etc
- implement the option to put additional logic for complete and error of an observable
Basic performance optimisations
- consider scheduling over
AnimationFrameSchedulerthe output is always for the view- handling changes could be done programmatically. Good for running zone-less
Implement strict and consistent handling of null/undefined for the bound value
Please visit the section Input Binding for a full list of requirements
Already existing similar packages:
---
_Receive events from child component over (stateChange)="fn($event)"_
In the parent component, we can receive events from child components over specific template syntax, the round brackets (stateChange).
Angular automatically updates fires the provides function over change detection.
In this way, we can receive component events.
Imperative approach:
@Component({
selector: 'my-app',
template: `
state: {{state}}
<app-child (stateChange)="onStateChange($event)"></app-child>
`
})
export class AppComponent {
state;
onStateChange(e) {
this.state = e;
}
}
Reactive approach:
@Component({
selector: 'my-app',
template: `
state: {{state$ | async}}<br>
<app-child (stateChange)="state$.next($event)"></app-child>
`
})
export class AppComponent {
state$ = new Subject();
}
Needs:
As it is minimal overhead we can stick with creating a Subject on our own.
No need for custom extensions
Due to the fact of the minimal overhead and the resources of creating a customDecoratorfor it there no need for as extension
---
As the component's logic can partially rely on the components life cycle hooks we also need to consider the in-out evaluation.
Angular fires a variety of lifecycle hooks. Some of them a single time some of them only once a components lifetime.
Angulars life cycle hooks are listed ere in order:
(Here the Interface name is used. The implemented method starts with the prefix 'ng')
The goal here is to find a unified way to have single shot, as well as ongoing life cycle hooks, and observable.
Imperative approach:
@Component({
selector: 'app-child',
template: `<p>change: {{changes | json}}</p>`
})
export class ChildComponent implements OnChanges {
@Input()
state;
changes;
ngOnChanges(changes) {
this.changes= changes;
}
}
Reactive approach:
As above mentioned in section Input Decorator we replay the latest value to avoid timing issues related to life cycle hooks.
@Component({
selector: 'app-child',
template: `<p>change: {{changes$ | async | json}}</p>`
})
export class ChildComponent implements OnChanges {
@Input() state;
onChanges$ = new ReplaySubject(1);
changes$ = this.onChanges$
.pipe(map(changes => changes));
ngOnChanges(changes) {
this.onChanges$.next(changes);
}
}
Handle general things for hooks:
Following things need to be done for every lifecycle hook:
@Component({
selector: 'app-child',
template: `<p>change: {{changes$ | async | json}}</p>`
})
export class ChildComponent implements OnChanges {
@Input() state;
onDestroy$$ = new ReplaySubject(1);
onDestroy$ = this.onDestroy$$.pipe(catchError(e => EMPTY));
onChanges$$ = new ReplaySubject(1);
onChanges$ = this.onChanges$$.pipe(catchError(e => EMPTY), takeUntil(this.onDestroy$));
ngOnChanges(changes) {
this.onChanges$.next(changes);
}
ngOnDestroy(changes) {
this.onDestroy$.next(changes);
}
}
Handle hook specific stuff:
To handle the differences in lifecycle hooks we follow the following rules:
@Component({
selector: 'app-child',
template: `<p>change: {{changes$ | async | json}}</p>`
})
export class ChildComponent implements OnChanges {
@Input() state;
const singleShotOperators = pipe(
take(1),
catchError(e => of(void)),
takeUntil(this.onDestroy$)
);
const ongoingOperators = pipe(
catchError(e => EMPTY),
takeUntil(this.onDestroy$)
);
onChanges$ = this.onChanges$$.pipe(this.ongoingOperators);
onInit$ = this.onInit$$.pipe(this.singleShotOperators);
doCheck$ = this.doCheck$$.pipe(this.ongoingOperators);
afterContentInit$ = this.afterContentInit$$.pipe(this.singleShotOperators);
afterContentChecked$ = this.afterContentChecked$$.pipe(this.ongoingOperators);
afterViewInit$ = this.afterViewInit$$.pipe(this.singleShotOperators);
afterViewChecked$ = this.afterViewChecked$$.pipe(this.ongoingOperators);
onDestroy$ = this.onDestroy$$.pipe(take(1));
ngOnChanges(changes) {
this.onChanges$.next(changes);
}
ngOnDestroy(changes) {
this.onDestroy$.next(changes);
}
}
Needs
We need a decorator to automates the boilerplate of the Subject creation and connect it with the property away.
Also subscriptions can occur earlier than the Host could send a value we speak about "early subscribers".
This problem can be solved as the subject is created in with instance construction.
Boilerplate Automation
For every binding following steps could be automated:
- setting up a
Subject- hooking into the
setterof the input binding and.next()the incoming value- hiding observer methods form external usage
Respect Lifetime and State of Lifecycles
- subscription handling tied to component lifetime
- single shot observables complete after their first call
Late Subscribers
- As subscriptions could happen before values are present (subscribing to
OnInitin the constructor)
we have to make sure the Subject is created early enough for all life cycle hooks- on subscription to already completed observable of a lifecycle it should return the last event and complete again.
---
In general, services are global or even when lazy-loaded the are not unregistered at some point in time.
The only exception is Services in the Components providers
Their parts of the services logic could rely on the life of the service, which is exactly the lifetime of the component.
Angular for such scenarios angular provides the OnDestroy life cycle hook for classes decorated with @Injectable.
The goal here is to find a unified way to have the services OnDestroy life cycle hooks as observable.
Imperative approach:
@Component({
selector: 'app-child',
template: ``,
providers: [LocalProvidedService]
})
export class ChildComponent implements OnChanges {
constructor(private s: LocalProvidedService) {
}
}
export class LocalProvidedService implements OnDestroy {
constructor() {
}
ngOnDestroy(changes) {
console.log('LocalProvidedService OnDestroy');
}
}
Reactive approach:
@Component({
selector: 'app-child',
template: ``,
providers: [LocalProvidedService]
})
export class ChildComponent implements OnChanges {
constructor(private s: LocalProvidedService) {
}
}
@Injctable({
providedIn: 'root'
})
export class LocalProvidedService implements OnDestroy {
onDestroy$ = new Subject();
constructor() {
this.onDestroy$subscribe(_ => console.log('LocalProvidedService OnDestroy');)
}
ngOnDestroy(changes) {
this.onDestroy$.next();
}
}
Needs
We need a decorator to automates the boilerplate of the Subject creation and connect it with the property away.
Boilerplate Automation
For every binding following steps could be automated:
- setting up a
Subject- hooking into the
setterof the input binding and.next()the incoming value- we should NOT override but EXTEND the potentially already existing functions
---
We propose adding an additional package to ngRx to support a better reactive experience in components.
We will manage releases of these packages in three phases:
@Input()Based on the above listing and their needs we suggest a set of Angular extensions that should make it easier to set up a fully reactive architecture.
Extensions suggested:
An angular pipe similar to the async pipe but triggers detectChanges instead of markForCheck.
This is required to run zone-less. We render on every pushed message.
(currently, there is an isssue with the ChangeDetectorRef in ivy so we have to wait for the fix.
The pipe should work as template binding {{thing$ | push}}
as well as input binding [color]="thing$ | push" and trigger the changes of the host component.
<div *ngIf="(thing$ | push) as thing">
color: {{thing.color}}
shape: {{thing.shape}}
<div>
<app-color [color]="(thing$ | push).color">
</app-color>
Included Features:
AnimationFrameScheduler (on by default)The *let directive serves a convenient way of binding multiple observables in the same view context.
It also helps with several default processing under the hood.
The current way of handling subscriptions in the view looks like that:
<ng-container *ngIf="observable1$ | async as c">
<app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
</app-color>
</ng-container>
The *let directive take over several things and makes it more convenient and save to work with streams in the template
*let="{o: o$, t: t$} as s;"
<!-- observables = { color: observable1$, shape: observable2$, name: observable3$ } -->
<ng-container *let="observable as c">
<app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
</app-color>
</ng-container>
<ng-container *let="observable; let c">
<app-color [color]="c.color" [shape]="c.shape" [name]="c.name">
</app-color>
</ng-container>
<ng-container *let="observable; color as c; shape as s; name as n">
<app-color [color]="c" [shape]="s" [name]="n">
</app-color>
</ng-container>
Included Features:
*ngIf="{}" normally effects it)async pipe AnimationFrameScheduler (on by default)A thing which turns a lifecycle method into an observable and assigns it to the related property.
The thing should work as a proxy for all life cycle hooks
as well as forward passed values i.e. changes coming from the OnChanges hook.
onInit$; // ??? very elegant and intuitive way to get an observable from a life-cycle hook
onDestroy$; // ??? very elegant and intuitive way to get an observable from a life-cycle hook
this.onInit$
.pipe(
switchMapTo(interval(1000)),
map(_ => Date.now()),
takeUntil(this.onDestroy$)
)
.subscribe();
Included Features
An operators selectChanges to select one or many specific slices from SimpleChange.
This operator can be used in combination with onChanges$.
It also provides a very early option to control the forwarded values.
Example of selectSlice operator
export class MyComponent {
// ??? very elegant and intuitive way to get an observable from a life-cycle hook
onChanges$: Observable<SimpleChanges>;
@Input() state;
state$ = this.onChanges$.pipe(getChange('state'));
}
Following things are done under the hood:
currentValue from SimpleChanges objectA property decorator which turns component or directive input binding into an observable and assigned it to the related property.
@Component({
selector: 'app-child',
template: `<p>input: {{input$ | async}}</p>`,
})
export class ChildComponent {
// ??? very elegant and intuitive way to get an observable from a life-cycle hook
input$;
}
Following things are done under the hood:
A property decorator which turns a view event into an observable and assigns it to the related property.
The solution should work do most of his work in the component itself.
Only a small piece in the template should be needed to link the view with the component property.
@Component({
selector: 'app-child',
template: `<button>clicks: {{count$ | async}}</button>`,
})
export class ChildComponent {
// ??? very elegant and intuitive way to get an observable from a life-cycle hook
click$;
count$ = this.click$.pipe(scan(a => ++a, 0));
}
Following things are done under the hood:
Here a link to a similar already existing ideas from @elmd_:
https://www.npmjs.com/package/@typebytes/ngx-template-streams
The most important message we need to teach developers is the basic usage of the new primitives.
The rest is up to pure RxJS knowledge.
This new library carries:
Is this going to be extending angular component implementation? Not completely follow.
Thanks for sharing this RFC, I really like your ideas 😄
@Tibing what exactly? Is there a feature that is missing?
@BioPhoton, I mean I like your idea to make Angular truly reactive. I still didn't get too deep to the RFC, whereas from the first sight, I have the following questions:
@OnChanges$() onChanges$: Observable<SimpleChanges>;
But why do we need it if we have Observable inputs? As I see it properly, Observable inputs have to fire before onChanges$. I think I didn't get something 🙃
@ViewChild() which selects an HTML element by id. Frankly speaking, I have no idea what to do with that. But, maybe, an idea will come in your mind.As I stated before, I didn't get in your RFC too deep, so, please, don't treat my comments too serious. I'm going to play with the reactive approach in Angular during the weekend and then I'll be able to add more constructive comments.
This is a really great idea! These things would make it frictionless to work with Observables properly in Angular, it's always a pain to write so much boilerplate when it would be so natural to use observables (@Input(), ngOnChanges practically screams Observable value to me).
Some previous discussion on the input topic can be found here as well: https://github.com/angular/angular/issues/5689
+1, these are great ideas that definitely make reactive programming easier with Angular.
Since it's highly related, I also want to give a shout-out to my own library @lithiumjs/angular that implements many of these ideas through new decorators that can be applied to component properties, inputs, outputs, host listeners, lifecycle events, etc.
A great idea indeed!
What if you just start with the directives? I feel like multiple packages might be better than a single, large package containing everything.
We actually forgo ngOnChanges entirely now - at my workplace we use the following directives to hook up @Input() to a BehaviorSubject:
````
@ObservablePropertyAccessor()
_sampleProp$ = new BehaviorSubject
@Input()
sampleProp: number;
````
The decorator essentially adds a getter / setter which allow reading / writing to the underlying BehaviorSubject.
The above code desugars to:
````
_sampleProp$ = new BehaviorSubject
@Input()
get sampleProp(): number { return this._sampleProp$.value; }
set sampleProp (value: number) { this._sampleProp$.next(value); }
````
We'd definitely use something which provided improvements to the templates. However, the decorators for lifecycle hooks might prove to be superfluous.
@Tibing
@HostBinding. You stated that reactive approach as TBD, but, do you have currently any ideas on
how to do it? I mean, maybe you already have some drafts but don't want to share them? 😄
No ATM I didn't investigate in int.
@hook$ I don't like the idea of having one decorator with the lifecycle hook name parameter. I
would personally prefer to have multiple decorators. That approach will allow us to avoid
mistyping and have autocompletion in IDE (Yep, I know we can make it strictly typed even with
strings but anyway). Also, it'll lead to more concise code:
@OnChanges$() onChanges$: Observable;
getChanges I would personally prefer to have getChange accepting a selector function instead of
a string (or we can have an overload, and it can do both). Because it could be useful in cases when
you need to select more that one simple change at a time.
I also like the idea of having one decorator per hook.
But why do we need it if we have Observable inputs?
We may need lifecycle hooks in addition to observable input bindings
@FromView$ I don't like the semantics of that decorator the same way as I don't like to use
@ViewChild() which selects an HTML element by id. Frankly speaking, I have no idea what to do
with that. But, maybe, an idea will come in your mind.
The goal here is to get observables from the view. Button clicks, inputs, any dom events, any angular event binding i.e. (click)
@bryanrideshark
What if you just start with the directives? I feel like multiple packages might be better than a single, large package containing everything.
The package contains only stuff for components/directives
We actually forgo ngOnChanges
Actually all lifacycles hooks are considered under section "Component and Directive Life Cycle Hooks"
at my workplace we use the following directives
@ObservablePropertyAccessor()
_sampleProp$ = new BehaviorSubject<number>(undefined);
@Input()
sampleProp: number;
2 things I can say here:
A) you should use a ReplaySubject(1) instead of a BehaviorSubject(initValue).
You don't need an initial, but just the latest value.
B) pulling out a value form an Observable over .value or having any getter function that returns a value or a new Observable is definitely wrong. We just want to subscribe to Observables.
We want to avoid imperative programming
I would be interested in feedback on 2 extensions:
push pipe*ngrxLet directiveHere a review of the suggested features, as well as general feedback, would be nice. :)
I know that the NgRx team will agree that we should not add additional decorators. Angular uses decorators heavily, but they are removed at compile time, since they are only used to instruct the compiler, create injectors, and so on.
Decorators is still a non-standard feature. It might never make it into ECMAScript. Even if it does, syntax and semantics might change.
Several of the use cases you suggest should be doable using Ivy _features_ such as this fromStore feature I created as a proof of concept.
@BioPhoton For the push pipe, we should make sure not to trigger/queue multiple change detection cycles from the same change detection cycle. Ivy's markForCheck does this.
Meaning, if two components depend on the same observable and a new value is emitted, only a single change detection cycle should occur right after the value emission as a result of the push pipe.
This could be done by calling ApplicationRef#tick exactly once after every change detection cycle where ChangeDetectorRef#markForCheck was called by push pipe.
@BioPhoton in the "Output Decorator" section do you mean btnClick.next($event) instead of
clickEmitter.next($event)?
@LayZeeDK This would only be for Ivy applications and it would be using markForCheck under the hood. We should be getting batching/scheduling for free as a result.
A high level constraint I'd like to place on the RFC: we should avoid decorators as much as possible. You can't type check them and they break lazy loading. If we have to use decorators for some of these APIs then we should take a similar approach to the AOT compiler or ngx-template-streams: apply code transforms to de-sugar them and strip the decorators.
Nice work @BioPhoton! Just had sometime to read through this.
I am sure more thoughts will come to me as time goes on and I digest more, but the first thought I am having is that I would love to see a take on the lifecycle hooks that doesn’t require a decorator.
For example, a more functional solution like:
‘onInit(()=>{...});’ etc...
These could be wired up in the constructor of the component.
This could gain inspiration from the new Vue 3.0 proposal.
Thoughts? Is it even technically possible?
@wesleygrimes, as far as I know, it is possible. However, setting up everything al the time in the CTOR is something repetitive that I try to avoid.
I guess we should investigate in the suggestion from @MikeRyanDev here
@MikeRyanDev regarding the change-detection, It should have a flag to detach CD and trigger it manually (zone-less), but by default, it should work as the async pipe.
@wesleygrimes, as far as I know, it is possible. However, setting up everything al the time in the CTOR is something repetitive that I try to avoid.
I guess we should investigate in the suggestion from @MikeRyanDev here
@MikeRyanDev regarding the change-detection, It should have a flag to detach CD and trigger it manually (zone-less), but by default, it should work as the async pipe.
Agreed on the CTOR part, could just be a class field initialized with onInit$ = onInit(() => {...})
I've come back to this because I wanted the *let structural directive to exist, but alas, it does not at this time.
I still feel like there are a lot of wins to be had, just by releasing things piecemeal.
Is there an initial proof of concept for the *let directive? If there is, I wouldn't mind being able to see it.
Hi @bryanrideshark
The let directive is more or less ready. We have to clarity some open questions etc..
The poc is in the component branch.
Let's me know what you think about the context infos of the stream (error, complete)
I think the concept is great. The only thing I don't understand: Why make this part of ngrx? I feel this is something that is not related to ngrx in any way apart from "they both use rxjs heavily". On the other hand I understand that in this repo you will find more feedback/activity/attention.
Hi @dummdidumm, from the start NgRx has always been about more than just state management. We are a platform for reactive technologies. Ng = Angular + Rx = Reactive. With this new addition we are continuing with that trend to expand our platform even more. We want to provide reactive ways for angular applications to be constructed.
I created NgObservable as a way to tackle most of these issues within with current compiler constraints.
The suggestions I make below however also include hypothetical APIs that would require changes to Angular itself.
Inputs should not be turned into observables. It's better to use the ngOnChanges lifecycle hook. Angular already gives you a lot of information here (current and previous value of keys that changes), so to get an observable stream of a particular value you can filter the ngOnChanges stream to select the value you want.
interface Props {
title: string
}
@Component()
class Component extends NgObservable implements Props {
@Input()
public title: string
constructor() {
ngOnChanges<Props>(this).pipe(
select((changes) => changes.title)
).subscribe((title) => {
// strongly typed value
console.log(title.currentValue)
})
}
}
With changes to the framework it shouldn't be necessary to extend a base class just to provide lifecycle hooks
interface Props {
title: string
}
@Component({
features: [OnChanges] // based on hostFeatures in Ivy
})
class Component implements Props {
@Input()
public title: string
constructor() {
ngOnChanges<Props>(this).pipe(
select((changes) => changes.title)
).subscribe((title) => {
// strongly typed value
console.log(title.currentValue)
})
}
}
Similar to markDirty(), it should be possible to implement similar methods for each of the lifecycle hooks in Angular. You could do this without breaking or changing the existing imperative API.
ngOnChanges() in this example returns an observable that behaves exactly like the class method would. features: [OnChanges] is the suggested change to the @Component() decorator needed to tell
the compiler to include the code needed to run the lifecycle hook.
No API change. Same as OP.
Using an RxJS subject that can also be called like a function, it is possible to adapt some of the existing Angular APIs to turn them into observable streams without changing any of the framework code.
// Invoke subject signature
export interface InvokeSubject<T> extends Subject<T> {
(next: T): void
(...next: T extends Array<infer U> ? T : never[]): void
}
class Component {
@HostListener("click", ["$event"])
public listener = new InvokeSubject<Event>
constructor() {
this.listener.subscribe((event) => {
console.log(event)
})
}
}
If changing the framework is feasible, this could be implemented as a hook too in a way that mirrors the fromEvent() operator from RxJS.
class Component {
constructor() {
hostListener(this, "click", ["$event"], { useCapture: true })subscribe((event) => {
console.log(event)
})
}
}
No API Change. Host bindings are just part of the component snapshot which are updated on change detection runs anyway, so there's no need to make this an observable.
To perform change detection automatically, we need to know when the state of the component changes. The best way to do this is with a dedicated "State" subject. This subject would be to components what Router is to the Angular routes (we then treat the component instance as a "stateSnapshot").
Currently there's a bit of ceremony needed to achieve this using the library I developed.
@Component({
providers: [StateFactory, Stream]
})
class Component extends NgObservable {
@Input()
title: string // the current or "snapshot" value
constructor(@Self() stateFactory: StateFactory, @Self() stream: Stream) {
const state = stateFactory.create(this)
// imperative API
// queues change detection to run next application tick()
// works like React setState basically
state.next({ title: "Angular" })
// reactive API
// automatically cleans up subscription when component destroyed
stream(state)(ngOnChanges(this).pipe(
select((changes) => changes.title),
map((title) => ({ title }))
))
// bonus: observe changes to entire component
state.subscribe(snapshot => console.log(snapshot))
}
}
With Angular Ivy and other framework changes this could be simplified.
@Component({
features: [NgOnChanges]
})
class Component {
@Input()
title: string // the current or "snapshot" value
constructor(stateFactory: StateFactory) {
const state = stateFactory.create(this)
state.next({ title: "Angular" })
stream(state)(ngOnChanges(this).pipe(
select((changes) => changes.title),
map((title) => ({ title }))
))
state.subscribe(snapshot => console.log(snapshot))
}
}
Remove all async logic from the template and everything becomes much simpler. Treat the component instance as a snapshot of the current state, then set good defaults and handle undefined behaviour with *ngIf or safe navigation operators ?.
There are {{totalActiveUsers}} online now.
<ng-container *ngIf="user">
Name: {{user.firstName}}
Surname: {{user.lastName}}
</ng-container>
Hi my name is {{user?.firstName}} {{user?.lastName}}
interface User {
firstName: string
lastName: string
}
@Component()
class Component {
user: User
totalActiveUsers: number
constructor(userSvc: UserService, stateFactory: StateFactory) {
const state = stateFactory.create(this)
this.totalActiveUsers = 0
this.user = null
stream(state)({ user: userSvc.getCurrentUser(), totalActiveUsers: userSvc.getActiveUsersCount() })
}
}
How this works is that whenever a new value is streamed to the state subject, state.next(partialValue) is called which then patches the values on the component instance (aka. the state snapshot), and then schedules change detection to update the template.
The same technique mentioned in HostListener can be used here as well.
@Component({
template: `
<app-child (stateChange)="onStateChange($event)"></app-child>
`
})
class Component {
onStateChange = new InvokeSubject<StateChange>
constructor() {
this.onStateChange.subscribe((stateChange) => console.log(stateChange))
}
}
These are currently implemented in NgObservable using a base class that implements all of the lifecycle hook methods and maps them to a subject. The "hook" methods then look for those subjects
on the component class instance to do their magic.
In Ivy and beyond, I think having feature flags is the best way to express our intent to use lifecycle hooks without explicitly putting them on the component class. This would require changes from
Angular's end.
See Input Decorator for what I mean.
Services created at the module level could just inject a provider that registers a single call to ngOnDestroy or ngModuleRef.onDestroy(). Services created at the component level could benefit from
a ngOnDestroy() hook that works the same as with components, or should just be completely stateless (let the component clean up subscriptions).
I think that more work should be done on Angular's side to enable the desired behaviour in a way that's performant and ergonomic without breaking or mutating existing APIs.
The NgObservable library was the best I could do given Angular's current compiler limitations, I hope this gives you some ideas.
Hi @stupidawesome!
Thanks, soo much for your feedback!
Input Decorator vs ngChanges:
Inputs should not be turned into observables. It's better to use the ngOnChanges lifecycle hook. > Angular already gives you a lot of information here (current and previous value of keys that
changes), so to get an observable stream of a particular value you can filter the ngOnChanges
stream to select the value you want.
In the section "selectChanges RxJS Operator" it is mentioned that this would be similar to input.
I proposed separate, very specific things because of 2 reasons:
The above document is here to speed up an old discussion and I hope to get some feedback from @robwormald in the next week(s).
Output Binding
I really like the InvokeSubject a lot because it also solves the HostListener thing with just a bit more code than nessacary for events from templates. This is really the best/smallest approach that I saw so far because it's just a function.
If I understand it correctly this is also smaller and way easier to implement than (ngx-template-streams)[https://github.com/typebytes/ngx-template-streams]. If true, I would go with this instead of the approach from @typebytes.
I think that more work should be done on Angular's side to enable the desired
behavior in a way that's performant and ergonomic without breaking or mutating existing APIs.The NgObservable library was the best I could do given Angular's current compiler limitations,
I hope this gives you some ideas.
I think so too @stupidawesome!
I spent and still spend a lot of time on this topic and get some change.
This RFC is the outcome of many discussions. IMHO, unfortunately, Angular will not ship anything related to this topic in the near future.
(@robwormald please correct if wrong)
Service Life Cycle Hooks and State Subject
I explicitly excluded from this RFC but wrote a lot about it in another document are:
This service helps to manage the component internal state (local state).
It does implement:
setState(value) method (not that good but I guess it helps beginners)connectState(observable) method (this is how ngRx/store should word if you ask me)We decided against pulling in this topic at the current state because it would just bring up too many discussions. Therefore I skipped all the information related to it here.
I'm a little late to the party, but how about an API like this:
https://github.com/dolanmiu/sewers/blob/master/src/app/card/card.component.ts#L5-L8
Essentially it's a type safe decorator to handle all your Observables, called a "Sink"
@Sink<CardComponent>({
obs: 'obs$',
data: 'data$'
})
@Component({
selector: 'app-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.scss'],
})
export class CardComponent {
@Input() public obs$: Observable<string>;
public readonly data$: Observable<string>;
public data: string;
public obs: string;
constructor() {
this.data$ = of('hello');
}
}
So in the above example, it will auto handle the observable myObservable$ and data$, and create variables in the component called ordinaryVariable and data. It even work's with @Input, which I think can be handy when piping observables into a component!
Source: https://github.com/dolanmiu/sewers/blob/master/projects/sewers/src/lib/sink.decorator.ts
Based on this talk by @MikeRyanDev:
https://github.com/MikeRyanDev/rethinking-reactivity-angularconnect2019
While I like that the decorator-approach is more natural for mixins, I feel that the concrete implementation is a little too verbose and one doesn't know whats going on if he does not know the conventions.
I think, with how Angular internally works (needing the LifeCycleHooks as methods on the class, not able to add them later on or tell the compiler to call it because we know it's there), the best approach is inheritance. In order to have reusability, one can also use inheritance with mixins. Based on the talk by @MikeRyanDev and using mixins:
import {
Component,
OnInit,
OnDestroy,
ɵmarkDirty as markDirty
} from '@angular/core';
import { Subject, Observable, from, ReplaySubject, concat } from 'rxjs';
import { scan, startWith, mergeMap, tap, takeUntil } from 'rxjs/operators';
type ObservableDictionary<T> = {
[P in keyof T]: Observable<T[P]>;
};
type Constructor<T = {}> = new (...args: any[]) => T;
const OnInitSubject = Symbol('OnInitSubject');
export function WithOnInit$<TBase extends Constructor>(Base: TBase) {
return class extends Base implements OnInit {
private [OnInitSubject] = new ReplaySubject<true>(1);
onInit$ = this[OnInitSubject].asObservable();
ngOnInit() {
this[OnInitSubject].next(true);
this[OnInitSubject].complete();
}
};
}
const OnDestroySubject = Symbol('OnDestroySubject');
export function WithOnDestroy$<TBase extends Constructor>(Base: TBase) {
return class extends Base implements OnDestroy {
private [OnDestroySubject] = new ReplaySubject<true>(1);
onDestroy$ = this[OnDestroySubject].asObservable();
ngOnDestroy() {
this[OnDestroySubject].next(true);
this[OnDestroySubject].complete();
}
};
}
export function WithConnect<
TBase extends Constructor &
ReturnType<typeof WithOnDestroy$> &
ReturnType<typeof WithOnInit$>
>(Base: TBase) {
return class extends Base {
connect<T>(sources: ObservableDictionary<T>): T {
const sink = {} as T;
const sourceKeys = Object.keys(sources) as (keyof T)[];
const updateSink$ = from(sourceKeys).pipe(
mergeMap(sourceKey => {
const source$ = sources[sourceKey];
return source$.pipe(
tap((sinkValue: any) => {
sink[sourceKey] = sinkValue;
})
);
})
);
concat(this.onInit$, updateSink$)
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => markDirty(this));
return sink;
}
};
}
export class Base {}
const ReactiveComponent = WithConnect(WithOnDestroy$(WithOnInit$(Base)));
@Component({
selector: 'app-root',
template: `
<div class="count">{{ state.count }}</div>
<div class="countLabel">Count</div>
<button class="decrement" (click)="values$.next(-1)">
<i class="material-icons">
remove
</i>
</button>
<button class="increment" (click)="values$.next(+1)">
<i class="material-icons">
add
</i>
</button>
`
})
export class AppComponent extends ReactiveComponent {
values$ = new Subject<number>();
state = this.connect({
count: this.values$.pipe(
startWith(0),
scan((count, next) => count + next, 0)
)
});
pushValue(value: number) {
this.values$.next(value);
}
}
This gives us the ability to define convenience-classes like ReactiveComponent while giving the user the ability to mixin other functionality as needed, mitigating the multiple inheritance problem. So he could do stuff like this:
export class SomeOtherBaseClass {
// ...
}
const MyCustomBaseClass = WithOnChanges$(WithConnect(WithOnDestroy$(WithOnInit$(SomeOtherBaseClass))))
@Component({...})
export class MyComponent extends MyCustomBaseClass {
// ...
}
@BioPhoton thanks for the tip about the ReplaySubject. I'll certainly look into doing that.
Hello :) I took a little peek into the angular compiler - components are core to angular functionality, it would take significant effort to create an alternative component. The current compiled data structure does not support bindings as inputs.
Information available about directives after compilation
export interface CompileDirectiveSummary extends CompileTypeSummary {
type: CompileTypeMetadata;
isComponent: boolean;
selector: string|null;
exportAs: string|null;
inputs: {[key: string]: string}; // !!! Inputs are stored here !!!
outputs: {[key: string]: string};
hostListeners: {[key: string]: string};
hostProperties: {[key: string]: string};
hostAttributes: {[key: string]: string};
providers: CompileProviderMetadata[];
viewProviders: CompileProviderMetadata[];
queries: CompileQueryMetadata[];
guards: {[key: string]: any};
viewQueries: CompileQueryMetadata[];
entryComponents: CompileEntryComponentMetadata[];
changeDetection: ChangeDetectionStrategy|null;
template: CompileTemplateSummary|null;
componentViewType: StaticSymbol|ProxyClass|null;
rendererType: StaticSymbol|object|null;
componentFactory: StaticSymbol|object|null;
}
Inputs are expressed as key value pairs - the key represents the property name on the component and the value represents the property in the template. Angular will have no way to distinguish between an input observable and a regular one. They cannot change how regular inputs work either for backwards compatibility.
A possible solution with realistic timelines
Make one property input state.
@Input({
[propertyName]: templatePropertyName
}) readonly input$ = new BehaiviorSubject/ReplaySubject<...>({});
Why?
1) This is a solution that would leverage the existing angular codebase - the only difference is that it would add inputPropertyName (or some other key name) to the data.
checkAndUpdateDirectiveInline would have to be modified slightly and some other areas need to pass the inputPropertyName to it.
2) This allows the input observable to essentially behave as ngOnChanges.
showButton$ = input$.pipe(tap(() => <onChangesLogic>))
3) The input subject can easily be split into multiple observables without tampering with the original ( it stays a subject, thus it keeps next).
showButton$ = input$.pipe(map(input => coerceBooleanProperty(input.showButton)));
regularBacon$ = input$.pipe(map(input=> input.spicy));
atomBomb$ = input$.pipe(tap(input => {
this.propagate(input.atom);
}));
customer$ = input$.pipe(map(input => input.customer));
order$ = combineLatest([
this.customer$,
this.store.pipe(select(state => state.order.registry))
]).pipe(map(([customer, registry]) => registry.find(order => order.customerID === customer.id))))
Who?
I would love if someone was willing to do handle the angular politics to get it implemented - not in my priorities right now.
Hope this helps - may you be happy.
Hi @OlaviSau.
Thanks for the answer!
Regarding your "who?" section,
I would love if someone was willing to do handle the angular politics to get it implemented - not in my priorities right now.
actually
@robwormald is handling this. He is also in the ngrx core team.
Thanks to @BioPhoton, @MikeRyanDev and all the others for contributing to this great RFC that has high potential to guide Angular & ngrx in the right direction.
For anyone new to this who aims to bring in helpful ideas please look at @MikeRyanDev's slides on this topic.
I will share my ideas and a small PoC library focused on using @ngrx Selectors directly in the template w/o async pipe until end of this month.
Hi @mikezks!
Thanks for your feedback.
As there is also component state you want to combine with the global state a selector in the template is nothing i would support.
Compostion needs to happen in component or component local provided services.
However, feel free to suggest all your ideas!
Hi @BioPhoton,
Yes, I agree with your argument.
I have been using the mentioned library internally for a while, but it has definitely not that broad scope for reactive development compared to the concepts discussed in this RFC, but can be used for Smart Components which are mainly getting their data out of the store and update it via Actions.
So I think this RFC's concept will lead to a more generic approach for RxJS in general and not ngrx statemanagement only. Nevertheless, let us continue the discussion whether some of the ideas may help after I published an example.
Hi @mikezks,
Definitely share your thoughts.! Even if i created the RFC and IMHO have a big picture, there might be things that are not covered in the RFC.
I.e. Manage component state, which I excluded from this RFC. I should post it I guess. This is the place where all the tiny problems come together...
@mikezks one more thing.
You mentioned the talk of @MikeRyanDev at angular connect, which is really a nice piece of information!
But the solutions are avoiding reactive programming because they subscribe and assign the value to a class property.
Class properties can't get composed further.
At the time we subscribe we end reactive programming
So (nearly) every solution that contains .subscribe and is not a pipe or a directive avoids reactive programming. Which is not the goal of this RFC.
@BioPhoton, yes I agree with that.
Nevertheless if @MikeRyanDev is planning to promote and implement this approach, we should definitly discuss this here as well. Chances are low, that several ideas will be published within the official @ngrx library.
Moreover IMO your concepts are not that far away. As @MikeRyanDev is directly referring to this RFC I would assume it is also his idea to find a solution with a broad acceptance.
Assigning Observable-Event-Values to class properties leads to state which is disconnected from the Observable-Stream, but actually the async-Pipe and the *ngIf-as-syntax are doing similar things. It is definitly important to use the assigned properties readonly resp. that the Observable-Stream is the only allowed source to update the value.
EDIT: the following remark does not really fit to this RFC, although important IMO.
Another important aspect is to discuss a concept on how to dispatch actions to the store with less boilerplate code. Currently this is still more afford than calling normal methods. At least for this my mentioned library offers one possible approach via Directives.
Hi again @mikezks.
What the RFC here is focusing on is the primitives that are missing in Angular. I on purpose put no "helper functions" there. Only primitives that should be provided by Angular.
... we should definitely discuss this here as well.
Yes. In general, the motivation should be focusing on primitives that can be used to create such ideas as connection a store to a property. In addition, the whole connecting is a very nice thing as log as it is the last thing you do with the incoming values. Because they are not composable anymore.
... but actually, the async-Pipe and the *ngIf-as-syntax are doing similar things...
Yes and it's not only the same thing but the important part there is the right place.
In the template.
There is (nearly) no case where I have to use .subscribe in my components.
For me the whole thing made more sense after I experimented with zone-less angular.
Another important aspect is to discuss a concept on how to dispatch actions to the store with less boilerplate code. Currently this is still more affordable than calling normal methods. At least for this, my mentioned library offers one possible approach via Directives.
As parts of ngRx/store are a pretty imperative (calling dispatch is a setter) I would do a tiny change to the store:
New Implementation with connectAction:
this.store
.connectAction(this.btnClick$.pipe(map(v => action(v))));
What happens here is defining the what. Defining the process that should run.
I hand the process over to somebody else that later on composes it and also runs it.
I should not be responsible for handling the how...
Current Implementation with dispatch:
const sub = this.btnClick$
.subscribe(v => this.store.dispatch(action(v)));
...
sub.unsubscribe();
I'm interested in your library and will ready the source as soon as you shared it. I also believe it will give another good point of view. <3
I agree with @mikezks that the connect-Method of @MikeRyanDev mentioned in his talk is equal if not superior to the push pipe. You don't manually (un)subscribe to anything, you just call the method. The added advantage is that you can also use the current state inside method calls if you like and don't have to pass it from the template back to the invoked methods. You also have less ceremony/noise inside the template. Most importantly, you make the transition from less-reactive components and the interaction with non-observable-things seamless: It all becomes a simple variable in the component. But I also agree with @BioPhoton in the sense that the connect method should be a primitive, i.e. not connected to ngrx/store. I envision a method which you can pass any observable(s) into:
@Component({
...
template: `
<p>Simple evaluation of property without any template noise: {{ state.bar }}</p>
<button (click)="logStuff()">Logg stuff</button>
`
})
export class SomeComponent {
bar$ = new Subject();
state = connect({
foo: foo$,
bar: bar$,
});
constructor(private foo$: Observable<any>){}
logStuff() {
console.log('currently foo is', this.state.foo);
}
}
Proof of concept with a fixed subject name, using dynamic updates.
You can convert inline to dynamic by using a spread for the values.
export function checkAndUpdateDirectiveDynamic(view: ViewData, def: NodeDef, values: any[]): boolean {
const providerData = asProviderData(view, def.nodeIndex);
const directive = providerData.instance;
let changed = false;
let changes: SimpleChanges = undefined !;
const inputState: any = {};
for (let i = 0; i < values.length; i++) {
inputState[def.bindings[i].nonMinifiedName!] = WrappedValue.unwrap(view.oldValues[def.bindingIndex + i]);
if (checkBinding(view, def, i, values[i])) {
changed = true;
inputState[def.bindings[i].nonMinifiedName !] = values[i];
changes = updateProp(view, providerData, def, i, values[i], changes);
}
}
const inputSubjectName = "input$";
if(changed && directive[inputSubjectName]) {
directive[inputSubjectName].next(inputState);
}
if (changes) {
directive.ngOnChanges(changes);
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
}
return changed;
}
Expanding on my earlier submission, I've taken some inspiration from Svelte and React to create a more ergonomic API for reactive state variables.
See this gist for a reference implementation.
How I use it:
@Component({
selector: "my-component",
template: `
<input [value]="name" (change)="setName($event.target.value)">
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent implements OnChanges, OnDestroy {
@Input()
public name: string
public age: number
public setName: InvokeSubject<string>
constructor(store: Store<any>) {
// Important! Variables must first be initialised before calling useState()
// Once TypeScript 3.7 lands we can use probably omit this
// See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier
this.name = undefined
this.age = undefined
this.setName = new InvokeSubject()
// Creates a getter/setter class that automatically subscribes to observables, maps them
// to the component and triggers change detection
const state = useState(this)
// Bind to individual properties
state.name = store.pipe(state => state.name)
// Or connect the entire state
connect(state, store.pipe(({ name, age }) => ({ name, age })))
// React to changes on properties
state.name
.pipe(
switchMap(name => interval(1000)),
// Requires call to useState() and dispose()
takeUntilDestroy(this),
)
.subscribe(count => console.log(`Name changed to "${this.name}" ${count} seconds ago`))
// We can bind multiple times to same property
state.name = this.setName
}
public ngOnChanges() {
// Connect state to input changes
connect(useState(this), of(this))
}
public ngOnDestroy() {
// Clean up observables, completes takeUntilDestroy
dispose(this)
}
}
Compared to before, this doesn't rely on extending a base class, injecting providers or reactive lifecycle hooks. It leverages the experimental markDirty API from Ivy to trigger change detection, with an option to pass in changeDetectorRef for older versions of Angular.
I like that you get around the "wait for ngOnInit"-problem by using a promise, although I don't know if this is stable enough. Is it ensured that angular will always do the initialization synchronous?
I think the API is a little confusing, especially when knowing react's hooks already, because it works different (useState called multiple times, no [state, setState]) but looks very similar. For me the API is not clear/intuitive/slim enough yet.
What I really like is how easy it is to make all properties of this observable and then using that.
If you want a working solution now. You can use store just by combining it with input or you can even replace the input - it's very simple once it's in the user land as an observable / subject.
@Component({
selector: "my-component",
template: `
<input [value]="name">
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent implements OnChanges {
@Input() name: string;
...; // other inputs
state$: Subject | BehaviorSubject | ReplaySubject;
ngOnChanges() { // ngOnChanges runs before ngOnInit btw :)
this.state$.next({name: this.name, ... // other inputs });
}
}
@dummdidumm Thanks for the feedback, it helped a lot.
I realised that I shouldn't blindly trigger change detection whenever a value changes. So I removed change detection from the consuming side and moved it to the producing side. The value producers should call markDirty() where appropriate to update the view.
Your point about useState not being in line with your expectations is valid. I looked around and decided that what I'm doing here is more akin to Object.observe(), so I've made changes accordingly.
I also swapped out the getter/setter with an ES6 Proxy. This removes the need to intialise variables on the class before attaching observers to it. Granted this comes at the cost of requiring a polyfill for older browsers.
Then I started thinking about how variables are assigned synchronously inside the template or component (eg. this.name = "name"). This kind of change isn't automatically propagated to the state observable, so I added a helper function that acts as a light wrapper around the markDirty api.
Here's an updated gist. I didn't add support for changeDetectorRef this time but it wouldn't be hard to.
Updated example:
@Component({
selector: "my-component",
// If state change within template emitters is desired, assign values then call markDirty
// `markDirty(prop = value)` is equivalent to `prop = value; markDirty()`
template: `
<input [value]="name" (change)="markDirty(name = $event.target.value)" />
<input [value]="age" readonly />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CardboardComponent implements OnInit, AfterViewInit, OnDestroy {
@Input()
public name: string
public age: number
@Output()
public ticker: EventEmitter<string>
public markDirty: MarkDirty
constructor(private store: Store<any>) {
// using Proxy objects, we don't have to initialise all class members before calling this,
// at the cost of requiring a polyfill for older browsers
const changes = observe(this)
// a helper function to propagate imperative value changes to state derived from `observe(this)`.
// Eg: this.age = 20; this.markDirty()
this.markDirty = markDirty
this.ticker = new EventEmitter()
// Calls to markDirty in the constructor must be deferred or it throws an error.
// Or just subscribe in ngOnInit instead
changes.age = store.pipe(
map(state => state.age),
markDirtyOn(this, asapScheduler)
)
}
public ngOnInit() {
// Create a changes observable for each property on the class
const changes = observe(this)
// Emits value of "name" property once on subscribe and every time it changes thereafter
changes.name
.pipe(
switchMap(() => interval(1000)),
map(count => `name changed to ${this.name} ${count} seconds ago`),
takeUntilDestroy(this)
)
.subscribe(this.ticker)
// Map store emissions of "name" to the component "name" property
// markDirtyOn calls markDirty each time a value is emitted
// This pattern can be repeated to derive reactive state chains
changes.name = this.store.pipe(
map(state => state.name),
markDirtyOn(this)
)
}
public ngOnChanges() {
// Propagate input changes to state derived from `observe(this)`
markDirty(this)
}
public ngOnDestroy() {
// Cleans up subscriptions and triggers takeUntilDestroy to prevent memory leaks
dispose(this)
}
}
How about something like this? Reverse bindings should be possible too.
import { ReplaySubject } from 'rxjs';
export function observeProperty(property) {
return (target: any, key: string) => {
let subjects = new Map();
let values = new Map();
Object.defineProperty(target, property, {
get() {
return values.get(this);
},
set(value: any) {
if (subjects.has(this)) {
subjects.get(this).next(value);
}
values.set(this, value);
}
});
Object.defineProperty(target, key, {
get() {
return subjects.get(this);
},
set(value: any) {
subjects.set(this, value);
}
});
};
}
class Example {
property = 1;
@observeProperty("property") property$ = new ReplaySubject<number>(1);
}
let example = new Example;
example.property$.subscribe(value => console.log(value));
console.log("setting value");
example.property = 2;
After thinking a bit more, I realized that moving the implementation to construction time will provide a better solution. It's a rough draft.
export function observeProperty$<T, K extends keyof T>(object: T, key: K): Observable<T[K]> {
const subject = new ReplaySubject<T[K]>(1);
let currentValue: T[K] = object[key];
Object.defineProperty(object, key, {
// tslint:disable-next-line:linebreak-after-method
set(value: T[K]) {
currentValue = value;
subject.next(value);
},
// tslint:disable-next-line:linebreak-after-method
get() {
return currentValue;
}
});
return subject;
}
class Example {
isLoading = false;
isLoading$ = observeProperty$(this, "isLoading");
}
This is an interesting approach, however accessing properties by strings e.g. "isLoading" is not property-renaming safe, as some JS code optimizers rename them.
@alex-okrushko In addition to that there is the issue of changing the accessor. If the observed prop already has accessors those will be lost. getOwnPropertyDescriptor would have to be used to apply the previous accessors.
About the property renaming - I am not sure how to solve that well. In theory a symbol / string could be used to define the prop itself, but that would create boilerplate. Any ideas how to solve it well?
it seems that lots of things is on the board
I like @MikeRyanDev's approach. Why not a have purely functional approach? (similar to markDirty)
import { OnDestroy, ɵmarkDirty as markDirty } from '@angular/core';
import { untilDestroyed } from 'ngx-take-until-destroy';
import { from, Observable, ReplaySubject, Subject } from 'rxjs';
import { mergeMap, tap } from 'rxjs/operators';
type ObservableDictionary<T> = {
[P in keyof T]: Observable<T[P]>;
};
type SubjectDictionary<T> = {
[P in keyof T]: Subject<T[P]>;
};
export function connectState<C extends OnDestroy, T>(component: C, sources: ObservableDictionary<T>): T {
const sink = {} as T & { $: SubjectDictionary<T> };
const sourceKeys = Object.keys(sources) as Array<keyof T>;
for (const key of sourceKeys) {
sink.$[key] = new ReplaySubject<any>(1);
}
const updateSink$ = from(sourceKeys).pipe(
mergeMap(sourceKey => {
const source$ = sources[sourceKey];
return source$.pipe(
tap((sinkValue: any) => {
sink.$[sourceKey].next(sinkValue);
sink[sourceKey] = sinkValue;
}),
);
}),
);
updateSink$
.pipe(untilDestroyed(component))
.subscribe(() => markDirty(component));
return sink as T & { $: ObservableDictionary<T> };
}
Way shorter, fits on one page. Note; it does not wait for ngOnInit - but it could be added similar to ngx-take-until-destroy
2 thinks you should consider:
I'm telling you, we have to lobby the Angular team/decision makers to expose component features 😄
https://indepth.dev/component-features-with-angular-ivy/
2 thinks you should consider:
- You have to call CD when the value is already in the template.
- If you subscribe it's over with reactive programming. 😁
I think we should provide both a push pipe and a connect function to let the user decide. I think it is ok to subscribe earlier than in the template. I don't feel I lose a lot of reactivity then, mostly it makes the code easier to read also for devs not confortable with indepth rxjs.
So it's a technical problem. You have to have the value in the template otherwise you are one off. (gets very clear if u disable zone)
Loosing reactivity:
At the moment of subscription you lost All reactivity. The connect approach in the template is a very good way to avoid reactive programming. But this package should leverage it.
If you want to get rid of subscriptions you can read my last research on component state.
Here an article on how to avoid rxjs. It lists Mike's as well as Eliran example of how to avoid reactive programming. https://dev.to/angular/how-to-avoid-observables-in-angular-273h
At the moment of subscription you lost All reactivity. The connect approach in the template is a very good way to avoid reactive programming. But this package should leverage it.
While I love defining observables and using them in the template (as long as I don't have to reach for hacks such as *ngLet) ... there are times when subscribe is needed in the component code. In those cases it seems very wrong to have to do .pipe(take(1)).toPromise() to get a simple feature flag.
Besides .. there needs to be a subscribe somewhere. | async is just short for .subscribe. If we hide .subscribe in some library code it's exactly the same.
At the moment of subscription you lost All reactivity. The connect approach in the template is a very good way to avoid reactive programming. But this package should leverage it.
While I love defining observables and using them in the template (as long as I don't have to reach for hacks such as *ngLet) ... there are times when subscribe is needed in the component code. In those cases it seems very wrong to have to do
.pipe(take(1)).toPromise()to get a simple feature flag.Besides .. there needs to be a subscribe somewhere.
| asyncis just short for.subscribe. If we hide.subscribein some library code it's exactly the same.
Exactly my thoughts!
I don't think that I lose "all" reactivity if I subscribe in typescript rather than in the template. Everything above, the services etc. can still leverage rxjs. For me the component is the sink, and it is mainly syntactical if I subscribe in the template or in typescript. It also makes stuff easier a lot of the time if I need to make method calls for a certain item of that stream.
there are times when subscribe is needed in the component code
That's called a side effect. A good example would be a tick to refresh http.
This process has nothing to do in the template.
How about a method that is named 'hold' or 'holdEffect' or 'connectEffect' that belongs to a component or the app?
https://github.com/BioPhoton/research-reactive-ephemeral-state-in-component-oriented-frontend-frameworks/blob/master/src/app/examples/demo-basics/3/demo-basics-3.component.ts#L102
Here a POC:
https://github.com/BioPhoton/rxjs-state/blob/master/packages/rxjs/src/state.ts#L165-L171
(as long as I don't have to reach for hacks such as *ngLet)
Using *ngIf="o$ | async as o" is a hack. *ngrxLet="o$ as o" is not.
In any case, you can subscribe wherever you want for a side effect. Just clean it up!
And to render something you have to do it in the template.
If you do that there are no problems, off by one error or other bugs.
Also, it will work without zone.js (Which is a major goal here too).
To sum it up, subscribe or and detect your changes wherever you want as long as you are happy.
:)
I think the discussion is derailing a little into personal preferences (I'm guilty of that, too). From a library perspective I think we should definitely support both subscribing via *ngxLet in the template and something like connect in typescript, because both have their valid use cases.
Sure! As you see from the provided examples it will at least land in the PR.
There as also no personal discussion. The off by one error and other things are facts.
Also both solutions are already in the PR.
I crarified certain problems. Where to detect changes, where to subscribe.
Could you clarify what you mean with "off by one error"? If I disable zone.js and do change detection myself, it works as expected https://stackblitz.com/edit/angular-5b4whw?file=src/app/app.component.ts . So a connect function should work, too.
I'll sit down on Monday.
Question, connect means subscribe and assign value to class prop?
If not you are covered. If yes, I assume it can be part of hmmm maybe store?
I see the use for cases where I want to receive a value from store and only directly render it without anything in between.
Yes, connect would mean something like that, basically what @MikeRyanDev proposed in his talk. This is what me and @Rush mean by subscribing in the component-typescript and not the template.
Well... It's avoiding rxjs. The main ideas of that package is leverage rx. No clue what will happen. I hope there is the right place to put this.
Would you be ok with assign the value on your own? Because than you can use the mentioned hold effect method. It's basically the same without assignment. (side effects never change state directly in a component)
If this is not fine for you, you need something ngrx/store specific. I guess opening an issue about adding this requested behavior to the store could be a solution.
Maybe I didn't get the poo t at the beginning. Hopefully now I understand your need: "assigning the value of an observable to a component property, nothing else in between" and it should be hidden away in the component.
| asyncis just short for.subscribe.
No, it's very much also about unsubscribing when the embedded view is destroyed. Preventing us from having to do manual subscription management is one of the major benefits, @BioPhoton is trying to achieve.
No, it's very much also about unsubscribing when the embedded view is destroyed. Preventing us from having to do manual subscription management is one of the major benefits, @BioPhoton is trying to achieve.
Agreed. It's just that for a long while I've been using an operator untilDestroyed(this) for all of my components.
The most important piece of logic in the AsyncPipe is the call to ChangeDetectorRef#markForCheck. However, that's also what makes it incompatible with NoopNgZone.
We fix this in NgrxPushPipe.
I thought about the *let hack not hack opinion...
I wrote quickly about the initial goal of *ngrxLet.
I hope it provides an idea of where it is used.
Please let me know if this makes sense.
The *ngrxLet directive serves a convenient way of binding observables to a view context (a template context).
It also helps with several internal processing under the hood.
The current way of binding an observable to the view looks like that:
<ng-container *ngIf="observableNumber$ as n">
<app-number [number]="n">
</app-number>
<app-number-special [number]="n">
</app-number-special>
</ng-container>
```
The problem is `*ngIf` is also interfering with rendering and in case of a `0`the component would be hidden
The `*let` directive take over several things and makes it more convenient and save to work with streams in the template
`<ng-container *let="observableNumber$ as c">`
```html
<ng-container *let="observableNumber$ as n">
<app-number [number]="n">
</app-number>
</ng-container>
<ng-container *let="observableNumber$; let n">
<app-number [number]="n">
</app-number>
</ng-container>
Included Features:
*ngIf="truthy$")async or ngrxPush pipezone.js is present or not (ChangeDetectorRef.detectChanges or ChangeDetectorRef.markForCheck)ChangeDetectorRef.detectChanges or ɵdetectChanges)Hi, @dummdidumm thanks you kept poking for the off by on error!!!
I ( as always :D ) was wrong! It was related to the *let directive! Also, I remember a second situation related to input bindings too when I used angular elements (in some version :D ).
For the connect thing:
I list some psudo like code on how to deal with it and don't need to care about subscription handling. It should show that one way can be to get an observable out and the other to assign a side effect to the observable.
Maybe you can give me some snippets back from what you would expect to be different so I can experiment a bit.
@Component({
selector: 'demo-basics-3',
template: `
{{param1}}
{{param2$ | async}}
`
})
export class DemoBasicsComponent3 extends LocalState<ComponentState> {
param1;
param2$ = this.something.select('param');
constructor(private something: Something, private route: ActivatedRoute) {
this.something.hold(
this.route.params,
( param ) => { this.param1 = param; }
);
this.something.connet(
'param2',
this.route.params
);
}
}
I'm slightly confused at this whole component situation. I've seen @MikeRyanDev talk about that higher order zoneless component quite a few times (ie. https://www.youtube.com/watch?v=rz-rcaGXhGk). Yet this issue seems to be talking about quite a few different concepts. Is this like a separate thing? Is there any order issue for Mike's thing?
I'm slightly confused at this whole component situation. I've seen @MikeRyanDev talk about that higher order zoneless component quite a few times (ie. youtube.com/watch?v=rz-rcaGXhGk). Yet this issue seems to be talking about quite a few different concepts. Is this like a separate thing? Is there any order issue for Mike's thing?
I don't know about "quite a few times". Did he present it elsewhere?
If you listen carefully, he says that he doesn't know what will be the right solution, that the code snippets are just one suggestion and that he needs the community's feedback. This is that feedback. An implementation without inheritance (thanks for that by the way @BioPhoton!)
I don't know about "quite a few times". Did he present it elsewhere?
Yeah, couple of local confs, it's mostly the same presentation.
What are cons to what Mike was proposing then? Is it losing reactivity?
Class properties can't get composed further.
At the time we subscribe we end reactive programming
So (nearly) every solution that contains .subscribe and is not a pipe or a directive avoids reactive programming. Which is not the goal of this RFC.
I might have misunderstood something, but I don't think that's a problem at all, you'd only connect the final result of a combination of whatever streams you have. At the point of connecting you should be done with reactivity / composing.
I've been doing something similar to dispatch action streams.
export class ProjectAddPage extends ZefReactiveComponent {
@ObservableEvent()
onAddProject$: Observable<Event>;
private _addProjectAction$ = this.onAddProject$.pipe(
formValueOnValid(this._projectInfoForm),
withLatestFrom(this._userEntity.activeClientUser$),
map(([ data, { clientId } ]) => this._projectEntity.addOne({ ...data, clientId }))
);
constructor(private _store: Store<State>) {
super();
this.dispatchActions$$(this._store, [
this._addProjectAction$
]);
}
}
doesn't feel like I lost any reactivity because at the point I'm passing those streams to that dispatcher I'm done with it.
An implementation without inheritance
That's a valid point, but won't this hopefully be solved https://indepth.dev/component-features-with-angular-ivy/ by the time best approach is decided?
@BioPhoton yes this is the direction I feel it should go in. Just some helpers that make (un)subscribing easy, so as @fxck said in his post above once we are done composing we can just use those helpers. If it's in the form of a component extension, or a service, or a decorator, or a base class -> guess this depends how easy to use those options are.
super.ngOnInit() if you need to use ngOnInit or one of the other hooks yourselfngComponentDef: would be nice, but not a public api yetWe share the same perspective. It's not called ngComponentDef anymore though.
Component features would allow a framework like this to create some nice APIs. However, there's currently no indication of intent to expose component definitions or features in public Angular APIs and they are marked as internal, meaning that they could have breaking changes in any version.
In the end, we can't do better than what that opinionated framework allows us to do.
Don't forget about base classes demanding any dependencies be injected through its constructor from the subclass. How would we access the Store if not through constructor injection? A service hides collaborating dependencies. A component feature can use directiveInject.
How would we access the Store if not through constructor injection?
Yeah, this is little unfortunate, but I think I read about a way to hack around it.
Found it: https://medium.com/better-programming/angular-inheritance-without-effort-8200c8d87972
Here the first draft of the documentation for the ngrxPush pipe.
Please let me know if this makes sense and I don't miss anything.
The ngrxPush pipe serves a drop-in replacement for the async pipe.
It contains intelligent handling of change detection to enable us
running in zone-full as well as zone-less mode without any changes to the code.
The current way of binding an observable to the view looks like that:
{{observable$ | async}}
<ng-container *ngIf="observable$ | async as o">{{o}}</ng-container>
<component [value]="observable$ | async"></component>
The problem is async pipe does not work in zone-less mode.
Heavy dynamic and interactive UIs suffer from zones change detection a lot and can
lean to bad performance or even unusable applications.
Also, async pipe triggers an ApplicationRef.tick which try's to rerender
the whole component tree. This draws performance.
ngrxPush pipe solves that problem. It can be used like shown here:
{{observable$ | ngrxPush}}
<ng-container *ngIf="observable$ | ngrxPush as o">{{o}}</ng-container>
<component [value]="observable$ | ngrxPush"></component>
Included Features:
zone.js is present or not (detectChanges or markForCheck)Also,
asyncpipe triggers anApplicationRef.tickwhich try's to rerender
the whole component tree. This draws performance.
To be precise, AsyncPipe only sets a dirty flag on the view and all ancestor views so that they will be dirty checked regardless of their change detection strategy.
Since AsyncPipe only sets a flag on views, but doesn't actually trigger a change detection cycle, it relies on logic initialized in ApplicationRef to run detectChanges on the root view which causes a full change detection cycle, every time an event or task is intercepted by NgZone.
This is why AsyncPipe as-is does not work well with NoopNgZone.
Here the design doc for the ngrxPush pipe:
https://hackmd.io/Uus0vFu3RmWRVGgmtzuUWQ?both=
I'm experimenting with a combination of base class + service injection + proxies.
Stackblitz example is here: https://stackblitz.com/edit/angular-9-0-0-rc-1-1qxbvs
All reactive state logic is handled by an effects class. The two parts to note here are the Reactive base class, and the effects provider.
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [effects(ChildEffects)]
})
export class ChildComponent extends Reactive {
@Input()
public name: string
@Input()
public age: number
@Output()
public ageChange = new EventEmitter()
public clicked: MouseEvent
}
In the effects class, each property marked by the @Effect() decorator should map onto the property of the same name to be updated in the component (except side effects).
Each effect method is a factory function that receives two arguments: a state object that maps each component property to an observable, and the component instance itself. Each method should return an observable of the same type as the corresponding state object (or a subscription if its a side effect). These methods are invoked during ngAfterViewInit to allow the component to be fully initialised.
Change detection can be controlled per property by setting markDirty: true, this could be extended to trigger detectChanges instead (thinking of zoneless change detection).
@Injectable()
export class ChildEffects implements Effects<ChildComponent> {
constructor(private http: HttpClient) {}
@Effect({ markDirty: false })
public name(state: State<ChildComponent>, component: ChildComponent) {
/** this.http.get("someUrl") // could do something with http here **/
return of("Stupidawesome")
}
@Effect({ markDirty: true })
public age(state: State<ChildComponent>, component: ChildComponent) {
return interval(1000).pipe(
withLatestFrom(state.age),
map(([_, age]) => age + 1),
tap(age => component.ageChange.emit(age)) // could be streamlined
)
}
// side effect
@Effect()
public sideEffect(state) {
return state.age
.subscribe(age => console.log(`age changed: ${age}`))
}
// Template events
// <p (click)="clicked = $event"></p>
@Effect()
public clicked(state, component) {
return state.clicked
.subscribe(event => console.log(`click:`, event))
}
}
The Reactive base class executes the effects and does cleanup after the component is destroyed.
Here the design doc for the *ngrxLet directive:
https://hackmd.io/8_3rp0A7RweSYJiulsifbQ?both
Here a collection of terms used in the document and their explanation:
https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10
Please let me know if I miss something!
Looks good @BioPhoton
btw. not sure if it's relevant but I feel having inputs as observables natively would be of help for the overall solution. I recently found this nifty library https://github.com/Futhark/ngx-observable-input
btw. not sure if it's relevant but I feel having inputs as observables natively would be of help for the overall solution. I recently found this nifty library Futhark/ngx-observable-input
Why this? Just pass an observable to the input property. Even change detection works by default when new values are emitted through input observables.
Why this? Just pass an observable to the input property. Even change detection works by default when new values are emitted through input observables.
Because you might be passing data down from a dumb component to a component that wants to utilise reactivity.
Because you might be passing data down from a dumb component to a component that wants to utilise reactivity.
You decide the data binding API, not your parent components 👴
Here's take 2 on my last post with comments on how it deals with the various features desired from a reactive component. For brevity I will only refer to components here, but this pattern could also be applied to directives and modules (anywhere you can add providers).
The main goal of this implementation is to develop a reactive API for Angular components with the following characteristics:
The API takes inspiration from NgRx Effects and NGXS. This example demonstrates a component utilising various angular features that we would like to make observable:
@Component({
selector: "my-component",
template: `
<div (click)="event = $event" #viewChildRef>Test</div>
`,
providers: withEffects(MyEffects),
host: { "(mouseover)": "event = $event" }
})
@RunEffects()
export class MyComponent {
@Input() count: number
@Output() countChange: EventEmitter<number>
@ViewChild("viewChildRef") viewChild: ElementRef | null
@ViewChildren("viewChildRef") viewChildren: QueryList<ElementRef> | null
public event: Event | null
constructor(@UseEffects() effects: Effects) {
this.count = 0
this.countChange = new EventEmitter()
this.viewChild = null
this.viewChildren = null
this.event = null
}
}
Binding the effects class is a three step process.
withEffects(Effects1, [Effects2, [...Effects3]])One or more classes are provided to the component that will provide the effects. Effects are decoupled from the component and can be reused.
@UseEffects() effects: EffectsEvery component using effects must inject the Effects class since there is no way to automatically instantiate a provider without it being injected.
@RunEffects()This decorator does two things. First it decorates all own properties of the component with getter/setter hooks so that changes can be observed. It then calls the Effects to run the effects. This happens _after_ the constructor is called.
We can work with one or more effects classes to describe how the state should change, or what side effects should be executed.
@Injectable()
export class MyEffects implements Effect<MyComponent> {
constructor(private http: HttpClient) {
console.log("injector works", http)
}
@Effect({ markDirty: true })
count(state: State<MyComponent>) {
return state.count.pipe(delay(1000), increment(1))
}
@Effect()
countChanged(state: State<MyComponent>, context: MyComponent) {
return state.count.subscribe(context.countChanged)
}
@Effect()
logViewChild(state: State<MyComponent>) {
return state.viewChild.changes.subscribe(viewChild => console.log(viewChild))
}
@Effect()
logViewChildren(state: State<MyComponent>) {
return queryList(state.viewChildren).subscribe(viewChildren => console.log(viewChildren))
}
@Effect()
logEvent(state: State<MyComponent>) {
return state.event.subscribe(event => console.log(event))
}
}
In this implementation, each method decorated by the @Effect() decorator will receive two arguments.
state: State<MyComponent>The first argument is a map of observable properties corresponding to the component that is being decorated. If the component has own property count: number, then state.count will be of type Observable<number>. Subscribing to this value will immediately emit the current value of the property and every time it changes thereafter. For convenience, the initial value can be skipped by subscribing to state.count.changes instead.
context: MyComponentThe second argument is the component instance. This value always reflects the current value of the component at the time it is being read. This is very convenient for reading other properties without going through the problem of subscribing to them. It also makes it very easy to connect to @Output().
There are three possible behaviours for each effect depending on its return value:
@Effect({ markDirty: true })
count(state: State<MyComponent>) {
return state.count.pipe(delay(1000), increment(1))
}
When an observable is returned, the intention is to create a stream that updates the value on the component whenever a new value is emitted. Returning an observable to a property that is not an own property on the class should throw an error.
@Effect()
logEvent(state: State<MyComponent>) {
return state.event.subscribe(event => console.log(event))
}
When a subscription is returned, the intention is to execute a side effect. Values returned from the subscription are ignored, and the subscription is cleaned up automatically when the effect is destroyed.
When nothing is returned, it is assumed that you are performing a one-time side-effect that does not need any cleanup afterwards.
Each effect method is only executed once. Each stream should be crafted so that it can encapsulate all possible values of the property being observed or mutated.
Because each effect class is an injectable service, we have full access to the component injector including special tokens such as ElementRef.
constructor(http: HttpClient) {
console.log("injector works", http)
}
We can delegate almost all component dependencies to the effects class and have pure reactive state. This mode of development will produce very sparse components that are almost purely declarative.
Lastly, the @Effect() decorator itself can be configured.
interface EffectOptions {
markDirty?: boolean
detectChanges?: boolean // not implemented yet
whenRendered?: boolean // not implemented yet
}
The first two options only apply when the effect returns an observable value, and controls how change detection is performed when the value changes. By default no change detection is performed.
The last option is speculative based on new Ivy features. Setting this option to true would defer the execution of the effect until the component is fully initialised. This would be useful when doing manual DOM manipation.
You might have noticed that there are no lifecycle hooks in this example. Let's analyse what a few of these lifecycle hooks are for and how this solution might absolve the need for them.
OnInitPurpose: To allow the initial values of inputs passed in to the component and static queries to be processed before doing any logic with them.
Since we can just observe those values when they change, we can discard this hook.
OnChangesPurpose: To be notified whenever the inputs of a component change.
Since we can just observe those values when they change, we can discard this hook.
AfterContentInitPurpose: To wait for content children to be initialised before doing any logic with them.
We can observe both @ContentChild() and @ContentChildren() since they are just properties on the component. We can discard this hook.
AfterViewInitPurpose: To wait for view children to be initialised before doing any logic with them. Additionally, this is the moment at which the component is fully initialised and DOM manipulation becomes safe to do.
We can observe both @ViewChild() and @ViewChildren() since they are just properties on the component. If that's all we are concerned about, we can discard this hook.
For manual DOM manipulation, there is another option. Angular Ivy exposes a private whenRendered API that is executed _after_ the component is mounted to the DOM. This is complimentary to the markDirty and detectChanges API that are also available, but not required for this solution. At this point in time there is no example to demonstrate how this might be used, but it is my opinion that once a reasonable solution is found we can discard this lifecycle hook too.
NgOnDestroyPurpose: To clean up variables for garbage collection after the component is destroyed and prevent memory leaks.
Since this hook is used a lot to deal with manual subscriptions, you might not need this hook. The good thing is that services also support this hook, do you could move this into the Effect class instead.
In the purely reactive world, components become much simpler constructs. With the power to extract complex logic into reusable functions this would result in components that are much more robust, reusable, simpler to test and easier to follow.
I am working on the implementation here: https://github.com/stupidawesome/ng-effects
Please, no more decorators 🙁
Imho reactive hooks is definitely a component feature in the future.
Please, no more decorators 🙁
interface MyState {
count: number
}
class MyEffects implements Effects<MyComponent> {
count = createEffect(
(state: State<MyState>, ctx: MyComponent) => interval(1000)),
{ markDirty: true }
)
}
@Component({
providers: [effects([MyEffects])]
})
class MyComponent implements MyState {
count: number
constructor(connect: Connect) {
this.count= 0
connect(this) // init any "connectable" services
}
}
How about this? Example code here
First pass at implementing a whenRendered hook:
It is invoked once immediately after the first round of change detection finishes.
@Effect({ whenRendered: true })
public viewChildren(state: State<TestState>) {
// is executed after view children have been attached
return state.viewChildren.subscribe(value => console.log("viewChildren available:", value))
}
Here @trotyl's hook:
https://github.com/trotyl/ng-reactive/blob/master/README.md
I've pushed the first unstable release of ng-effects to NPM.
Misko's Ivy design docs are interesting
Looks like zones are going away eventually.
Pushed a new version of ng-effects. It adds experimental support for zoneless change detection.
Effects can now also return partial object updates.
Another update to ng-effects, this time bridging the gap with Custom effect handlers
This is how you can link this lib with NgRx or other action dispatchers:
@Injectable({ providedIn: "root" })
export class Dispatch implements EffectHandler<Action, Options> {
constructor(private store: Store<any>) {}
next(value: Action, options: Options) {
this.store.dispatch(value)
}
}
@Injectable()
export class AppEffects {
@Effect(Dispatch)
dispatchAction(state: State<AppComponent>, context: AppComponent) {
return context.formData.valueChanges.pipe(
map(payload => ({
type: "FORM_UPDATED",
payload
}))
)
}
}
As of this update we probably don't need separate ngrx/effects or component facades anymore.
I try to get an opinion on passing the context. What are the pros and cons for you?
I try to get an opinion on passing the context. What are the pros and cons for you?
Local effects should be able to compose or connect Observables/EventEmitters in the component instance such as FormGroup and @Ouput(). This can be combined with the observable state (when component properties change) and observable services. This example demonstrates all the observable sources at play. Outputs in particular must be assigned in the constructor of a component otherwise Angular throws an error.
Overall however this is just a convenience. You could also obtain the reference by State and then switch mapping to it.
Passing in the context is something that is common practice using providers.
@Component({
providers: [{ provide: Parent, useExisting: ParentComponent }]
})
export class ParentComponent {}
@Component()
export class ChildDirective {
constructor(parent: Parent) {}
}
So there is nothing particularly novel about passing in the context. The library currently provides this in two ways: 1. as an argument to each effect method and 2. via the HostRef provider. The idea is to treat the context as just another observable source.
I haven't used or tested this library extensively enough to understand all of the downsides, but the biggest con is probably going to be the use of property interception to construct the State observables.
The downsides are that it won't be easy to extend a "connected" class. If we only connect "final" classes this can be mitigated (explore the prototype for effects higher up the chain?). The Connect mechanism itself is only there as a workaround to the lack of some sort of HOST_INITIALIZER token in the same vein as APP_INITIALIZER.
A side effect of using Object.defineProperty to do property interception is that this only works if the property is already initialized. I use proxies in dev mode to catch invalid property access early, but more importantly this solution necessitates that all properties be definitely assigned (even if set to undefined) in the constructor before calling connect. This might be jarring for some users even if it is generally a good practice to follow.
The plus side is that this lets us easily observe all property changes on the component regardless of where those changes originate (ie. template events, @Input() bindings, queries, dynamic component loaders, etc). If all properties can be observed, then we don't need to rely on lifecycle hooks or base classes to trigger updates. We can solve problems such as late subscribers (State is hot) and hot/cold/unicast/multicast observables (State is multicast, effects are only subscribed to once). Being able to read current values from the context inside effects is a big convenience compared to unwieldy alternatives such as withLatestFrom or combineLatest.
Additionally, we get fine tune control over when to trigger change detection. We can make change detection occur when some or all properties change, or we can use sampling/throttling to limit or disable change detection for a period of time. We can also ditch zones.
Biggest change: No more property interceptors! Internal state is now hooked directly into the change detection process. State is checked both before and after a change detection run, and any effects that depend on component state will be triggered if those properties have changed. State is also checked again after each effect has emitted a new value. This all happens in one pass before the view is marked dirty, so no unnecessary change detection should occur.
I realized that using property interceptors was not ideal for triggering change detection because simply assigning values to properties on a component should not trigger side effects. As a result of this change, imperatively assigning a value from inside or outside the component will not cause effects or change detection to run.
For imperative/dynamic components, effects will run automatically once you trigger change detection on them via changeDetector.markForCheck() or markDirty(componentInstance) (or whenever angular schedules this itself.
@stupidawesome thank you for putting these ideas into a library, but we're getting off-topic here with the updates. If you would like to discuss that library further, open an issue in your repo so people can continue discussing it there.
My 2 cents as a new programmer (just over a year now) for a small business as a one person team. One of the reasons I’ve fell in love with the ngrx modules has been that the code writers are not afraid to address complex ideas to newcomers through ng-conf talks, tonnes of examples and are willing to be explicit about what is good code and what is misuse and bad form. The time to learn is a much under appreciated measure shortened by an awesome and supportive community. So thank you. ❤️ Please keep sharing as you go and keep ability to test at the forefront.
I write a lot of boilerplate code to get a component to be reactive and sometimes leave it off in favour of using the angular hooks for smaller use cases which often feels clunky. Especially if I opt for the OnChanges hook which does feels ugly guarding with if statements to get the input I care about (obviously could use get and set but this adds a huge number of lines early in a component when all I want is to be able to immediately see the main component methods at a glance e.g. buildForm() and the formvalue emitter). I experimented with this medium post which felt natural to use but use of decorator was a massive no no. And repeating the same code to get a takeUntil(this.destroyed$). Had some success using class mixin for this but felt like overkill for a single benefit. I really can see the upside to this module and look forward to it. 🤓
I've implemented connectState as a simple function and exposed it as a library. I'm using it in production projects and it's working just fine. Decorator is based off @ngneat/until-destroy. Feel free to use in your project!
https://github.com/Rush/ng-connect-state/
import { ConnectState, connectState } from 'ng-connect-state';
import { interval } from 'rxjs';
@ConnectState()
@Component({ template: `
{{ state.timer }}
<button (click)="state.reload()">Reload</button>
Loading: {{ state.loading.timer }}
`
})
export class InboxComponent {
ngOnDestroy() { }
// this exposes state.timer as a synchronous value state.timer and automatically unsubscribes on destroy
// + it has a .loading indictor, exposes optional observable access as well as can resubscribe to observables on demand
state = connectState(this, {
timer: interval(1000),
})
}
Most helpful comment
Welcome to the party:
https://dev.to/rxjs/research-on-reactive-ephemeral-state-in-component-oriented-frameworks-38lk