ngrx / platform

Reactive State for Angular
https://ngrx.io
Other
8.01k stars 1.97k forks source link

RFC: Component: Proposal for a new package `component` #2052

Closed BioPhoton closed 4 years ago

BioPhoton commented 5 years ago

RFC: Component: Proposal for a new package component

Reactive primitives for components

https://github.com/ngrx/platform/pull/2046



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 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

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:

@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 setter of 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 ReplaySubject with bufferSize of 1 to emit notifications

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:

@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 an Observable as EventEmitters there is no need for as 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:

@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 setter of the input binding and .next() the incoming value

Early Producer
Make sure the created Subject it present early enough


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:

@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 setter of the input binding and .next() the incoming value

Early Subscribers
Make sure the created Subject it present early enough


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:

@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 the ChangeDetection and trigger it on every value change

Performance optimisations

  • consider scheduling over AnimationFrameScheduler the output is always for the view

Implement strict and consistent handling of undefined for pipes
A pipe similar to async that should act as follows:

  • when initially passed undefined the pipe should forward undefined as value as on value ever was emitted
  • when initially passed null the pipe should forward null as value as on value ever was emitted
  • when initially passed of(undefined) the pipe should forward undefined as value as undefined was emitted
  • when initially passed of(null) the pipe should forward null as value as null was emitted
  • when initially passed EMPTY the pipe should forward undefined as value as on value ever was emitted
  • when initially passed NEVER the pipe should forward undefined as value as on value ever was emitted
  • when reassigned a new Observable the pipe should forward undefined as 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:


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 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 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 as as 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 AnimationFrameScheduler the 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:


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:

@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 custom Decorator for it there no need for as extension


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:

@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 setter of 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 OnInit in 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.

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:

@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 setter of the input binding and .next() the incoming value
  • we should NOT override but EXTEND the potentially already existing functions

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:

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 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:

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:

<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:

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 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

selectChanges RxJS Operator

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:

Observable Input Bindings

A 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:

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.

@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

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

evgenyfedorenko commented 5 years ago

Is this going to be extending angular component implementation? Not completely follow.

Tibing commented 5 years ago

Thanks for sharing this RFC, I really like your ideas 😄

BioPhoton commented 5 years ago

@Tibing what exactly? Is there a feature that is missing?

Tibing commented 5 years ago

@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:

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 🙃

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.

ValentinFunk commented 5 years ago

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

lVlyke commented 5 years ago

+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.

only2dhir commented 5 years ago

A great idea indeed!

bryanrideshark commented 5 years ago

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.

BioPhoton commented 5 years ago

@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)

BioPhoton commented 5 years ago

@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

BioPhoton commented 5 years ago

I would be interested in feedback on 2 extensions:

Here a review of the suggested features, as well as general feedback, would be nice. :)

LayZeeDK commented 5 years ago

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.

LayZeeDK commented 5 years ago

@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.

VPashkov commented 5 years ago

@BioPhoton in the "Output Decorator" section do you mean btnClick.next($event) instead of clickEmitter.next($event)?

MikeRyanDev commented 5 years ago

@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.

MikeRyanDev commented 5 years ago

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.

wesleygrimes commented 5 years ago

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?

BioPhoton commented 5 years ago

@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 commented 5 years ago

@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(() => {...})

bryanrideshark commented 5 years ago

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.

BioPhoton commented 5 years ago

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)

dummdidumm commented 5 years ago

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.

wesleygrimes commented 5 years ago

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.

stupidawesome commented 5 years ago

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.

Input Decorator

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.

Output Decorator

No API change. Same as OP.

HostListener Decorator

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)
        })
    }
}

HostBinding Decorator

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.

Input Binding

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))
    }
}

Template Bindings

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.

Output Binding

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))
    }
}

Component and Directive Life Cycle Hooks

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.

Service Life Cycle Hooks

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).

Suggested Extensions under @ngRx/component Package

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.

BioPhoton commented 5 years ago

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:

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.

dolanmiu commented 5 years ago

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

dummdidumm commented 5 years ago

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 {
  // ...
}
bryanrideshark commented 5 years ago

@BioPhoton thanks for the tip about the ReplaySubject. I'll certainly look into doing that.

OlaviSau commented 5 years ago

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.

BioPhoton commented 5 years ago

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.

mikezks commented 4 years ago

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.

BioPhoton commented 4 years ago

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!

mikezks commented 4 years ago

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.

BioPhoton commented 4 years ago

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...

BioPhoton commented 4 years ago

@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.

mikezks commented 4 years ago

@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.

BioPhoton commented 4 years ago

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

dummdidumm commented 4 years ago

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);
  }
}
OlaviSau commented 4 years ago

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;
}
stupidawesome commented 4 years ago

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.

dummdidumm commented 4 years ago

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.

OlaviSau commented 4 years ago

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 });
    }
}
stupidawesome commented 4 years ago

@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)
    }
}
OlaviSau commented 4 years ago

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;
OlaviSau commented 4 years ago

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");
}
alex-okrushko commented 4 years ago

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.

OlaviSau commented 4 years ago

@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?

LinboLen commented 4 years ago

it seems that lots of things is on the board

Rush commented 4 years ago

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

BioPhoton commented 4 years ago

2 thinks you should consider: