Platform: @ngrx/store/init firing before effects are listening

Created on 19 Jul 2017  路  26Comments  路  Source: ngrx/platform

Since the upgrade, INIT actions are no longer being pickup up via any effects.

I've also confirm that I'mm using the EffectsModule.forRoot([AppEffects])

Test Setup
image

Most helpful comment

UPDATE

https://ngrx.io/guide/effects/lifecycle

Original Comment

If I use defer and return another Observable directly, it doesn't work.

    @Effect() init$ = Observable.defer(() => {
        return Observable.of({ type: RootActions.INIT });
    });

    @Effect() applicationInit$ = this._Actions$
        .ofType(RootActions.INIT)
        .switchMap((action: any) => this.initial());

So I double check how defer really works. http://reactivex.io/documentation/operators/defer.html

The Defer operator waits until an observer subscribes to it, and then it generates an Observable

I guess defer generates the Observable before next effect observer.

So if I change the sequence, everything works. (Just move init$ to the bottom of the file.)

I think defer is really just a hack.

All 26 comments

Ah, sorry! This is intentional but we forgot to mention it in the migration guide. You can refactor that action to look like this for the same behavior:

import { defer } from 'rxjs/observable/defer';

@Effect()
init$: Observable<Action> = defer(() => {
    console.log('ok');
    const username = '';
    const password = '';

    return of(new auth.LoginAction({ username, password });
  });

Will gladly accept a PR that adds a more thorough example to the migration guide!

Strange, if I use

@Effect() public readonly initEffect = Observable.defer(() => Observable.of(new Action1()));

then neither Action1, nor any other effect is executed. And if I remove initEffect, other effects works as expected. Probably I haven't migrated properly.

I have the same issue as @zygimantas

My project has a Init effect that uses defer(). After upgrading to ngrx v4 all actions after the Init effect are no longer run/working.

@brandonroberts @MikeRyanDev is that a bug and we should reopen this issue or "defer" is not the best way to do it?

UPDATE

https://ngrx.io/guide/effects/lifecycle

Original Comment

If I use defer and return another Observable directly, it doesn't work.

    @Effect() init$ = Observable.defer(() => {
        return Observable.of({ type: RootActions.INIT });
    });

    @Effect() applicationInit$ = this._Actions$
        .ofType(RootActions.INIT)
        .switchMap((action: any) => this.initial());

So I double check how defer really works. http://reactivex.io/documentation/operators/defer.html

The Defer operator waits until an observer subscribes to it, and then it generates an Observable

I guess defer generates the Observable before next effect observer.

So if I change the sequence, everything works. (Just move init$ to the bottom of the file.)

I think defer is really just a hack.

@maxisam I can confirm, that after moving the init$ with defer to the bottom, applicationInit$ is executed, BUT other actions, dispatched from component, using this.store.dispatch(new Action3()); are not handled by effects.

@zygimantas I don't have the problem you have.

I think another workaround is dispatching a init action in app root module instead of using defer.

Nice tip @maxisam ! Solved my problem with effects not executing after init+defer. This should be in the migration guide.

@maxisam can you give a quick sentence on how you do that? :)

Does startWith not work? example here

From what i gather we are trying to accomplish the same thing, on app startup perform something. In my electron app i use the startWith in my init effect to read in the json config file.

    // Reads and initializes the client from local config file if exists.
    @Effect() public init$: Observable<fromClientConfig.All>
    = this.actions$.ofType(fromClientConfig.LOAD)
        .startWith(new fromClientConfig.LoadAction())
        .switchMap(() => this.configService.readConf())
        .map((conf) => new fromClientConfig.InitSuccessAction(conf))
        .catch(err => of(new fromClientConfig.InitFailedAction()));

this then fires off this effect if it fails

 @Effect() public initFailed$: Observable<fromClientConfig.All>
    = this.actions$.ofType(fromClientConfig.INIT_FAILED)
        .switchMap(() => this.configService.writeConf(this.configService.defaults))
        .map((conf) => new fromClientConfig.InitSuccessAction(conf))
        .catch(err => of(new fromClientConfig.ConfigSaveFailedAction()));

@southeastcon2017 dispatching a init action in your app root module? You just need to inject store into your app root component and dispatch an action for your effect to work.

Or you can do it in onNgInit in app.component.ts, like you do in any other component, if that feels more natural than writing code directly in the module class.

One thing that appears to work for me right now is to inject the store into my effects class. Then use it from within the defer to dispatch the event I need.

@Injectable()
export class AppEffects {
  @Effect({dispatch: false} appInit$: Observable<any> = defer(() => {
    console.log('appInit effect')
    this.store.dispatch({ type: 'application_start', payload: { /* some app data */ })
  })

  // some more effects

  constructor(private actions$: Actions, private store: Store<any> {}
}

When I added defer to my effects class, the other effects stopped working. I believe this is the same issue that @zygimantas had. I tried moving the defer to the bottom of my effects class as suggested by @maxisam and that did not fix it. Next I tried moving the defer to a separate effects class. That also did not work _until_ I made the new effects class the last one in the EffectsModule.forRoot() call.

UPDATE
I got it to work by combining the @maxisam suggestion to put the init at the end _plus_ the @dfmartin technique of injecting the store into the effects class to dispatch the initialization action. This allowed me to eliminate the separate effects class that was just used for the defer initialization. Seems that there must be a better way...

Guys, this seems awfully fragile and as @maxisam said, this seems like a hack.

What works for me is creating an action derived class returning type INIT and using startWith. This is consistent with my other action classes.

import { INIT } from '@ngrx/store';
export class StoreInit implements Action {
get type() {
return INIT
}
}

and then

@Effect()
initSession$: Observable<Action> = this.actions$
    .ofType(INIT)
    .startWith(new StoreInit())
    .switchMap(action => this.authService.afAuth.authState)
    .mergeMap(auth => [new SessionStartSuccess(auth ? auth.uid : null)])

For us changing from @ngrx/store/init to @ngrx/effects/init worked.

import { ROOT_EFFECTS_INIT } from '@ngrx/effects' works

I agree with @rpm911 that the current solution is fragile at best. @MikeRyanDev is there a better solution than using defer()?

Wow. This is not the kind of thread you want to read when getting started on @ngrx.
Is there an official doc what we're supposed to actually do?

This was closed without a real solution? A bunch of hacks that only work in some cases? Effects/reducers on my app aren't being triggered at all. It'd be great if one of them maintainers did a write up of these common problems with a definitive explanation of these cases, now I have to try all these different approaches and hope one works for me. I'll blame it on myself for only upgrading ngrx > 4 in Nov 2018.

The problem with defer() is that while it delays creation of the observable until it is subscribed, the created observable will still push its value immediately. This will happen while NgRx is still in the process of subscribing to the defined effects, and before later effects have been subscribed to. This is why people are getting different results depending on the order they have defined their effects.

What we really need to do is delay the push of the action instead. The way to do this using RxJS is with a scheduler. Therefore, I would suggest not using defer() and instead using the RxJS asyncScheduler to delay dispatch of the action to a later event loop task, so NgRx has a chance to finish initializing all the effects before the value is pushed:

import { asyncScheduler, of } from 'rxjs';

@Effect()
$init = of(new LoginAction({ username, password }), asyncScheduler);

@jonrimmer Thanks, works as expected.

@jonrimmer , I have the latest version of NgRx, but when I use

@Effect()
$init = of(new notificationActions.loadNotifications(), asyncScheduler)

I get compile error Only a void function can be called with the 'new' keyword.

My action doesn't have payload:

export const loadNotifications = createAction(
    NotificationActionTypes.LoadNotifications,
)

Maybe I'm missing something?

@dmitriydementor you don't use new with an action creator. Just call the function.

Was this page helpful?
0 / 5 - 0 ratings