Closed BioPhoton closed 4 years ago
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>;
pull out currentValue from SimpleChanges object
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<number>(undefined);
@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<number>(undefined);
@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:
RFC: Component: Proposal for a new package
component
Reactive primitives for components
https://github.com/ngrx/platform/pull/2046
component
Table of Content
Summary
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.
Motivation
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 theasync
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
Overview
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:
Input Decorator
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:
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.
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 onOnChange
where the first moment where the view is ready would beAfterViewInit
Output Decorator
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:
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().Needs:
No need for an extension.
HostListener Decorator
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:
Reactive approach:
Needs:
We would need a decorator automates the boilerplate of the
Subject
creation and connect it with the property. Assubscriptions
can occur earlier than theHost
could send a value we speak about "early subscribers". This problem can be solved as the subject is created in with instance construction.HostBinding Decorator
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:
Reactive approach:
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 theHost
could be ready we speak about "early subscribers". This problem can be solved as the subject is created in with instance construction.Input Binding
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:
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 afterAfterViewInit
.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.Already existing similar packages:
Template Bindings
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 theasync
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.Binding over the
as
syntax To avoid such scenarios we could use theas
syntax to bind the observable to a variable and use this variable multiple times instead of using theasync
pipe multiple times.Binding over the
let
syntax Another way to avoid multiple usages of theasync
pipe is thelet
syntax to bind the observable to a variable.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
directiveasync
pipe*`ngIf` directive triggered by falsy values**
As we can see, in this example the
ng-container
would only be visible if the value is1
and thereforetruthy
. Allfalsy
values like0
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**
By using
*ngFor
to create a context variable we avoid the problem with*ngIf
andfalsy
values. But we still misuse a directive. Additionally*ngFor
is less performant than*ngIf
.Nested
ng-container
problemHere 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
Here we can use
*ngIf
again because and object is alwaystruthy
. However, the downside here is we have to use theasync
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
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.Already existing similar packages:
Output Binding
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:
Reactive approach:
Needs:
As it is minimal overhead we can stick with creating a
Subject
on our own.Component and Directive Life Cycle Hooks
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:
Reactive approach:
As above mentioned in section Input Decorator we replay the latest value to avoid timing issues related to life cycle hooks.
Handle general things for hooks:
Following things need to be done for every lifecycle hook:
Handle hook specific stuff:
To handle the differences in lifecycle hooks we follow the following rules:
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 theHost
could send a value we speak about "early subscribers". This problem can be solved as the subject is created in with instance construction.Service Life Cycle Hooks
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:
Reactive approach:
Needs
We need a decorator to automates the boilerplate of the
Subject
creation and connect it with the property away.Suggested Extensions under @ngRx/component Package
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:
Push Pipe
An angular pipe similar to the
async
pipe but triggersdetectChanges
instead ofmarkForCheck
. This is required to run zone-less. We render on every pushed message. (currently, there is an isssue with theChangeDetectorRef
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.Included Features:
AnimationFrameScheduler
(on by default)Let Structural Directive
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:
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;"
Included Features:
*ngIf="{}"
normally effects it)async
pipeAnimationFrameScheduler
(on by default)Observable Life Cycle Hooks
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 theOnChanges
hook.Included Features
selectChanges RxJS Operator
An operators
selectChanges
to select one or many specific slices fromSimpleChange
. This operator can be used in combination withonChanges$
.It also provides a very early option to control the forwarded values.
Example of selectSlice operator
Following things are done under the hood:
currentValue
fromSimpleChanges
objectObservable Input Bindings
A property decorator which turns component or directive input binding into an observable and assigned it to the related property.
Following things are done under the hood:
Observable Output Bindings
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.
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
How We Teach This
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.Drawbacks
This new library carries:
Alternatives