Platform: effects v4 not compatible with ngUpgrade

Created on 23 Aug 2017  路  13Comments  路  Source: ngrx/platform

I'm submitting a...


[X] Regression (a behavior that used to work and stopped working in a new release)
[X] Bug report  
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request

What is the current behavior?

EffectsFeatureModule and EffectsRootModule inject the effect in the module constructor. That means all the effects classes are instantiated during bootstrap of angular app.

When using ngUpgrade I bootstrap angular app first, and angularJS app second. My effect class depends on ngUpgraded service. Angular2+ attempts to create an instance of ngUpgraded service (and $injector) during angular bootstrap, before angularJS is bootstrapped.

This essentially prevents me from upgrading to NGRX v4 in the hybrid app.

Expected behavior:

EffectsModule doesn't inject the effects in the constructor.

Needs Reproduction Effects

Most helpful comment

I am also coming across this issue and would love a solution that's built into the API. The workaround @ukrukarg proposed is difficult to implement if no Angular component is being used. Another workaround is to provide the Effect classes as a specific token that handles multiple instances. Then, in ngDoBootstrap following the bootstrap of the AngularJS side of things, the injector gets the token and has each class add itself to the effects sources.

For reference:

Special effects token:

export interface RegisterEffects { ngRegisterEffects(): void; }

export const EFFECTS_CLASS =
    new InjectionToken<RegisterEffects[]>('EFFECTS_CLASS');

export function EffectsProvider<T extends RegisterEffects>(
    effectsClass: Type<T>): ClassProvider {
  return {
    provide: EFFECTS_CLASS,
    multi: true,
    useClass: effectsClass,
  };
}

ngRegisterEffects would look like this:

ngRegisterEffects() {
  this.sources.addEffects(this);
}

A provider is added like so:

EffectsProvider(EffectsClass),

And the ngDoBootstrap becomes:

this.upgrade.bootstrap(appName);

const effectClasses = this.injector.get(EFFECTS_CLASS);
for (const effectClass of effectClasses) {
  effectClass.ngRegisterEffects();
}

All 13 comments

@robwormald

Workaround I found that seems to work with ngUpgrade:

BEFORE:

@NgModule({
  imports: [EffectsModule.forFeature([MyEffects])],
  declarations: [MyComponent]
})
export class MyModule {}

AFTER:

@NgModule({
  providers: [MyEffects]
})
export class MyModule {}

@Injectable()
export class MyEffects {
  constructor(sources: EffectSources) {sources.addEffects(this);}
}

@Component()
export class MyComponent {
  constructor(effects: MyEffects) {} // injected just so effects get registered
}

I am also coming across this issue and would love a solution that's built into the API. The workaround @ukrukarg proposed is difficult to implement if no Angular component is being used. Another workaround is to provide the Effect classes as a specific token that handles multiple instances. Then, in ngDoBootstrap following the bootstrap of the AngularJS side of things, the injector gets the token and has each class add itself to the effects sources.

For reference:

Special effects token:

export interface RegisterEffects { ngRegisterEffects(): void; }

export const EFFECTS_CLASS =
    new InjectionToken<RegisterEffects[]>('EFFECTS_CLASS');

export function EffectsProvider<T extends RegisterEffects>(
    effectsClass: Type<T>): ClassProvider {
  return {
    provide: EFFECTS_CLASS,
    multi: true,
    useClass: effectsClass,
  };
}

ngRegisterEffects would look like this:

ngRegisterEffects() {
  this.sources.addEffects(this);
}

A provider is added like so:

EffectsProvider(EffectsClass),

And the ngDoBootstrap becomes:

this.upgrade.bootstrap(appName);

const effectClasses = this.injector.get(EFFECTS_CLASS);
for (const effectClass of effectClasses) {
  effectClass.ngRegisterEffects();
}

I believe the correct workaround is to simply add ngDoBootstrap to your root NgModule:

@NgModule({ ... })
export class RootModule {
  ngDoBootstrap() { }
}

@brandonroberts and I maintain a very large hybrid app and everything works as expected with that added to the root module.

Do you have a repository where I can view this hybrid app @MikeRyanDev? I have ngDoBootstrap in my app and the issue still appeared.

@michaelgerakis the repository is not public. Can you provide a minimal reproduction of this issue using plunker or a github repo?

@ukrukarg @michaelgerakis Can this issue be closed?

@brandonroberts I managed to update repro case by @pobrienms to reflect my setup better: https://github.com/ukrukarg/injectorRepro

Could this be reopened now?

Reopening. Will have a look

@ukrukarg Apologies for the inactivity on this issue. If this is still a problem with NgRx Effects v5 please open a fresh issue and we'll take a look!

I am having the same issue. Is there a workaround? I have same structure as @MikeRyanDev proposed I think*.

@NgModule({ ... })
export class RootModule {
  ngDoBootstrap() { }
}

This is my ngBootstrap

@NgModule({
  imports: [EffectsModule.forRoot(MyAwesomeEffects), AngularJSModuleProvider.forRoot()]
})
export class ApplicationShellModule {
  constructor(
    private upgrade: UpgradeModule,
    private store: Store<fromRoot.State>
  ) {}
  ngDoBootstrap(app: ApplicationRef){
    this.upgrade.bootstrap(
      document.querySelector('.spa-container'), [ajsModule.name], { strictDi: true });
      app.bootstrap(AppComponent); // This bootstraps angular 8
      this.store.dispatch(BootstrapActions.BootstrapSuccess()); // Sends bootstrap actions 
  }
}

This is my sample effect

import { Injectable, Inject } from '@angular/core';
import { Actions } from '@ngrx/effects';

@Injectable()
export class MyAwesomeEffects {
  constructor(
    @Inject('AngularJSService') private angularJSService: any,
    private actions$: Actions
  ) {}
}

As soon as I inject @Inject('AngularJSService') private angularJSService: any, I get

Screen Shot 2019-08-08 at 10 22 57 PM

I am on v8 of ngrx

"@ngrx/effects": "^8.1.0",
"@ngrx/router-store": "^8.1.0",
"@ngrx/store": "^8.1.0",
"@ngrx/store-devtools": "^8.1.0",

The solution that I generally use to workaround this is to lazy initialize the Injectable that I need. This only works if the effect that you're using the Injectable runs after AngularJS bootstraps, but for the most part this is working well enough for my purposes.

// Any lazy implementation will do, you can inline this if desired
// this one is adapted from https://www.dustinhorne.com/post/2016/05/09/factory-pattern-and-lazy-initialization-with-angularjs-and-typescript
export class Lazy<T> {
    private instance: T = null;
    private initializer: () => T;

    constructor(initializer: () => T) {
        this.initializer = initializer;
    }

    public get value(): T {
        if (this.instance == null) {
            this.instance = this.initializer();
        }

        return this.instance;
    }
}
import { Injectable, Injector } from '@angular/core';
import { Actions } from '@ngrx/effects';


@Injectable()
export class MyAwesomeEffects {
  constructor(
     private readonly injector: Injector,
    private actions$: Actions
  ) {
  private _angularJSService = new jsCommon.Lazy<IAngularJsService>(() => this.injector.get('AngularJSService'));

  private get angularJSService() { return this._angularJSService.value(); }

  myEffect$ = createEffect(() =>
    this.actions$.pipe(
      ofType(myActions.Type),
      mergeMap(action =>
        return this.angularJSService.someMethod(); // <== injection happens here
      )
    )
  );

  }
}

In this example, the AngularJS injector is only used when the block inside mergeMap runs. Effects classes are constructed pretty early in the lifetime of an Angular application. Depending on how you have your app configured, this can happen before you call upgrade.boostrap(). By deferring the injection of your Angular JS service until the effect actually runs you give Angular JS time to bootstrap.

Of course, there's still potential that your effect runs before upgrade.boostrap() is called and lazy initialization won't save you. In these cases, you have some options:

  • Port the service to Angular
    This is usually what I recommend to my coworkers if possible. If you need this service so early in the initialization of your app, that might be a good signal that you should prioritize moving it to your Angular app.
  • Wait for AngularJS initialization to start running the effect
    You can dispatch an action after AngularJS initialization and use something like skipUntil to not run your effect until that action is fired or use a service to keep track of initialization and wait on that. Combine this with the lazy initialization method above to coordinate the injection of your service. Of course this means that your effect is not running until this happens, so if you need it running earlier you're out of luck
  • Run AngularJS bootstrap earlier
    I can't confirm that this works, but Mike's comments above imply that there is a way to call upgrade.boostrap() so that it runs and initializes the AngularJS injector before the effects are registered.
Was this page helpful?
0 / 5 - 0 ratings

Related issues

mappedinn picture mappedinn  路  3Comments

gperdomor picture gperdomor  路  3Comments

smorandi picture smorandi  路  3Comments

ghost picture ghost  路  3Comments

brandonroberts picture brandonroberts  路  3Comments