Skip to content
This repository has been archived by the owner on Dec 26, 2017. It is now read-only.

WIP: refactor(Effects): Remove runEffects and StateUpdates, add NgModule #32

Closed
wants to merge 1 commit into from

Conversation

MikeRyanDev
Copy link
Member

@MikeRyanDev MikeRyanDev commented Aug 11, 2016

AKA the release where I delete most of this library.

In @ngrx/effects v1, providers were created dynamically via runEffects(...effects: Type | Type[]). However, the providers generated this way were not statically analyzable and so they would not work with the offline compiler.

I'm looking for comments on the new, offline compiler friendly API:

import { Injectable, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Store } from '@ngrx/store';
import { Effect, mergeEffects, Actions } from '@ngrx/effects';


@Injectable()
export class AuthEffects implements OnDestroy {
  subscription: Subscription;

  constructor(private actions: Actions, store: Store<T>) {
    this.subscription = mergeEffects(this).subscribe(store);
  }

  @Effect() login$ = this.actions$
    .ofType('login')
    .mergeMap(...)

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Note: services in RC5 are automatically instantiated if they implement the OnDestroy interface. This has the added benefit of automatically starting / stopping effects dynamically, like during routing.

This has been published with the @beta tag. Get it with npm install @ngrx/effects@beta

Also in this release is the new release structure outlined in ngrx/core#4

cc @fxck @btroncone @robwormald

@btroncone
Copy link
Contributor

LGTM 👍
You know I was fond of previous API but it's time has come. 😄

@fxck
Copy link

fxck commented Aug 11, 2016

Yeah, looks good to me. What I would like to see in README:

  • where has update$.state gone to and how to get it back when you need it
  • whether ngOnDestroy is required and how to instantiate the effect without it(when you want your effects to run always and everywhere)

as a side note:

@awerlang
Copy link

I think this is good. The previous implementation was a bit too much.

You know I was fond of previous API but it's time has come. 😄

It should be easy enough for users to reproduce the previous experience:

@Injectable()
export class BaseEffects implements OnDestroy {
  subscription: Subscription;

  constructor(private actions: Actions, store: Store<T>) {
    this.subscription = mergeEffects(this).subscribe(store);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

@Injectable()
export class AuthEffects extends BaseEffects {

  @Effect() login$ = this.actions$
    .ofType('login')
    .mergeMap(...)

}

The downside with this is when we need to inject more providers in the constructor.

What about creating an OnSubscribe interface and letting effect classes implementing it instead of initializing in constructors:

export interface OnSubscribe<T>() {
  onSubscribe(actions: Actions, store: Store<T>): void;
}

@Injectable()
export class BaseEffects implements OnSubscribe, OnDestroy {
  actions$: Actions;
  subscription: Subscription;

  onSubscribe(actions: Actions, store: Store<T>): void {
    this.actions$ = actions; // or inject in constructor
    this.subscription = mergeEffects(this).subscribe(store);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

@Injectable()
export class AuthEffects extends BaseEffects {

  @Effect() login$ = this.actions$
    .ofType('login')
    .mergeMap(...)

}

Yet another option to not rely on constructor in base class for acquiring the action$ source, is making effects, functions:

  @Effect() login$ = actions$ => actions$
    .ofType('login')
    .mergeMap(...)

WDYT?

@awerlang
Copy link

while you are at it, can we get that end, terminate, finish operator?

I'd prefer configuring the decorator:

@Effect(/* dispatch: boolean = true */ false) login$ = actions$ => actions$
    .ofType('login')
    .do(...)

@fxck
Copy link

fxck commented Aug 11, 2016

☝️ that looks reasonable.

@MikeRyanDev
Copy link
Member Author

where has update$.state gone to and how to get it back when you need it

Recreating the updates stream is pretty straightforward and makes it more clear how to apply selectors in effects:

actions$
  .ofType('login')
  .withLatestFrom(store$)
  .map(([ action, state ]) => ({ action, state }))

whether ngOnDestroy is required and how to instantiate the effect without it(when you want your effects to run always and everywhere)

Yes, as of today if you want your service to be automatically instantiated it must implement the OnDestroy interface.

What about creating an OnSubscribe interface and letting effect classes implementing it instead of initializing in constructors:

I don't think this is technically possible yet.

I'd prefer configuring the decorator:

Yeah, configuring the decorator like that seems like a reasonable improvement of the API. I think I'd prefer it this way to leave room for future configuration options:

@Effect({ dispatch: false }) login$ = this.actions$

since ofType presumably accepts multiple actions, how about naming it ofTypes instead

I'm following what they are doing in redux-observable. See redux-observable/redux-observable#95

@fxck
Copy link

fxck commented Aug 11, 2016

Recreating the updates stream is pretty straightforward and makes it more clear how to apply selectors in effects:

yes, and it's gonna be a source of confusion nevertheless, that's why it better be mentioned in readme :)

import { Injectable, Inject } from '@angular/core';
import { Action, Dispatcher } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { Operator } from 'rxjs/Operator';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem to be needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't fully implemented the typings for the lift method and importing the Operator interface will be required to do so. See: https://github.com/ngrx/store/blob/master/src/store.ts#L25

@meenie
Copy link

meenie commented Aug 12, 2016

I've tried @ngrx/effects@beta in my app and it works great! Only minimal changes to get it to work. I'm even getting access to the store and pulling out a value with something like:

  @Effect() redirectAfterLogin$ = this.actions$
    .ofType(AuthActions.REDIRECT_AFTER_LOGIN)
    .withLatestFrom(this.store.let(appSelectors.getAuthRedirectUrl()))
    .do(([action, url]) => router.navigateByUrl.next(url))
    .mapTo(this.authActions.resetRedirectAfterLogin())

withLatestFrom() is amazing :)

Great work!

@meenie
Copy link

meenie commented Aug 12, 2016

Due to the new version of Effects, I tried to see if I could inject the Router into my Effects Class and I'm getting this error: zone.js:461Unhandled Promise rejection: Bootstrap at least one component before injecting Router.. So I'm still having to use the RouterPatch hack.

Any thoughts on this?

@brandonroberts
Copy link
Member

@meenie can you make a plunker with this issue?

@MikeRyanDev
Copy link
Member Author

Problems with this proposed API:

  • Not currently possible to inject services that require the application to have been bootstrapped
  • Creating the effects subscription in the constructor makes the effects service harder to test
  • Requiring developers to implement the OnDestroy interface is confusing

I'll try to address these issues with a new API sometime early this week.

@btroncone
Copy link
Contributor

@Effect({ dispatch: false }) login$ = this.actions$

I really like this approach ☝️ 👍

@stefanoslig
Copy link

Is there any way to unit test an effect? I want to find a way to send an action to my effect. In the current version there is the sendAction function.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants