I'm submitting a...
[ ] Regression (a behavior that used to work and stopped working in a new release)
[] Bug report
[ ] Feature request
[ ] Documentation issue or request
[X] Support request
I am currently using a package called @ngx-config to bootstrap my application configuration via HTTP. I have some @Effects classes where I inject some services that use configuration that is bootstrapped and to get this configuration to work, I had to run these Effects with EffectsModule.runAfterBootstrap however now, with the upgrade to 4.X this no longer exists and now my configuration is not being bootstrapped prior to the effects being injected with services.
Any suggestions?
I am not familiar with @ngx-config so I'll provide a lot of code just so you can see what my situation is in comparison to yours. I also want to load in some runtime settings from a file on application startup. The following is working for me with the current version of effects. The mistake I was making initially was trying to pull out config properties in my services constructor but at this time they are still not initialized. Instead I just ask for them when needed and this won't occur until after the configuration service has been setup thanks to the APP_INITIALIZER token.
Hope this helps.
imports: [
BrowserModule,
FormsModule,
HttpModule,
StoreModule.forRoot(reducers),
EffectsModule.forRoot([ProductEffects])
],
providers: [
ConfigurationService,
{
provide: APP_INITIALIZER,
useFactory: init,
deps: [ConfigurationService],
multi: true
},
ProductsService
],
app.module
@Injectable()
export class ConfigurationService {
private _config: AppConfig;
constructor(private http: Http) { }
get apiEndpoint() {
return this._config.apiEndpoint;
}
load(): Promise<void> {
return new Promise((resolve) => {
this.http.get('appsettings.json')
.map(response => response.json())
.subscribe(config => {
this._config = config;
resolve();
});
});
}
}
export function init(configService: ConfigurationService): Function {
return () => configService.load();
}
configuration.service.ts
@Injectable()
export class ProductsService {
constructor(private http: Http, private config: ConfigurationService) { }
loadProducts(): Observable<Product[]> {
return this.http.get(this.config.apiEndpoint)
.map(response => response.json());
}
}
products.service.ts
export class AppComponent {
title = 'app works!';
products$: Observable<Product[]>;
constructor(private store: Store<fromRoot.State>) {
this.products$ = store.select(fromRoot.getProducts);
store.dispatch(new productActions.LoadProductsAction());
}
}
app.component.ts
Controlling effects if what you're looking for https://github.com/ngrx/platform/blob/master/docs/effects/api.md#onruneffects. You can dispatch an action once you're application is bootstrapped, then the effects will start listening for the desired actions. You can also stop effects from listening when a certain action is dispatched.
Hi Brandon, thanks for taking the time to respond.
When I view the definition of the OnRunEffects interface it is expecting a param and return type of Observable<EffectNotification> as opposed to an Observable<Action> as given in the example you linked to.
import { Observable } from 'rxjs/Observable';
import { EffectNotification } from './effect_notification';
export interface OnRunEffects {
ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>): Observable<EffectNotification>;
}
export declare function isOnRunEffects(sourceInstance: Object): sourceInstance is OnRunEffects;
This differs to the given example, is it possible to get an updated example for the use of this interface?
Sure. There is an issue in effects also as its not exporting EffectNotification. I'll update the docs and add this to the example app, but here is an updated example.
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/exhaustMap';
import { Injectable } from '@angular/core';
import { Action } from '@ngrx/store';
import { Effect, Actions, OnRunEffects, EffectNotification } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import * as auth from '../../auth/actions/auth';
@Injectable()
export class CollectionEffects implements OnRunEffects {
ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>) {
return this.actions$
.ofType(auth.LOGIN_SUCCESS)
.exhaustMap(() =>
resolvedEffects$.takeUntil(
this.actions$.ofType(auth.LOGOUT),
),
);
}
}
How does this fix effects being injected with services that need to be bootstrapped? I need something to be bootstrapped before Effects are bootstrapped. Not delay or stop an effect from listening to something.
@RangerDanger94 I will try getting my configuration bootstrapped with your method and try without ngx-config. Thanks for the suggestion :)
The bootstrapping example provided above doesn't work for me. The settings that are supposed to be set from Http don't get set before being injected into one of the Effects. So I still need to find a way to only run effects after.
So how can I actually inject the Setting Service, once it has been created properly. Before it was fine, now with v4 even with this approach it doesn't work.
app.effects.ts
@Injectable()
export class AppEffects {
@Effect()
public init$: Observable<Action> = defer(() => of(new UserActions.FetchUser()));
}
user.effects.ts
@Injectable()
export class UserEffects {
@Effect()
public fetch$: Observable<Action> = this.actions$
.ofType(UserActions.FETCH_USER)
.switchMap(() => {
return this.securityService.fetchUser()
.map((user) => new UserActions.FetchUserComplete(user))
.catch((error) => of(new UserActions.FetchUserFailed(mapError('FETCH_USER_FAIL', error))))
});
constructor(
private actions$: Actions,
private router: Router,
private securityService: SecurityService) {
}
}
settings.service.ts (BOOTSTRAP)
@Injectable()
export class SettingsService {
public manager: UserManager;
constructor(
private httpClient: HttpClient,
private storageService: StorageService,
private dispatcher: UserActionsDispatcher,
@Inject(ORIGIN_URL) private originUrl: string) {
}
public load(): Promise<UserManager> {
return new Promise((resolve) => {
this.httpClient.get(`${this.originUrl}/api/configuration/config`)
.subscribe(data => {
this.manager = new UserManager({
authority: data['authority'],
client_id: 'angular2Client',
redirect_uri: data['identityServerRedirectUrl'],
post_logout_redirect_uri: data['identityServerRedirectUrl'],
response_type: 'id_token token',
scope: 'openid profile',
silent_redirect_uri: this.originUrl + '/silent-renew-callback.html',
automaticSilentRenew: true,
accessTokenExpiringNotificationTime: 4,
filterProtocolClaims: true,
loadUserInfo: true,
stateStore: new WebStorageStateStore({ store: this.storageService }),
userStore: new WebStorageStateStore({ store: this.storageService })
});
resolve(this.manager);
});
});
}
}
Provider
export function loadSettings(settingsService: SettingsService): Function {
return () => settingsService.load();
}
export const Services = [
SettingsService,
{
provide: APP_INITIALIZER,
useFactory: (loadSettings),
deps: [SettingsService],
multi: true
},
...
]
Ok so I fixed it by removing the app.effects.ts and just adding a store.dispatch to the fetchUser in my app.component.ts. Works now.
Someone, help me understand why this issue has been closed. Effects classes (and their dependencies) being instantiated before APP_INITIALIZER is still a problem. I don't feel like implementing OnRunEffects is the proper solution here, as many of the Services that effects depend on could be configured in their constructors. I guess we could manually inject them with a service locator in OnRunEffects, but that seems like a bit of an anti-pattern.
I have to agree with you, but nobody seems to want to respond. So i had to change how it sort of works. Been weeks since I opened this. Never make a new product with new tech. :)
Effects aren't tied to the APP_INITIALIZER so they can be started as early as possible to begin listening for actions. The runAfterBootstrap API was removed because the reason it was added was longer needed. The Router used to need the application to be bootstrapped before you could inject it but that limitation has long been removed. If you want effects to wait until start after bootstrap, you'll need to register them using mergeEffects.
Example:
import {
NgModule,
APP_BOOTSTRAP_LISTENER,
InjectionToken,
Inject
} from '@angular/core';
import { Store } from '@ngrx/store';
import { EffectsModule, mergeEffects } from '@ngrx/effects';
import { merge } from 'rxjs/observable/merge';
import { SomeEffect } from './some.effects';
export const BOOTSTRAP_EFFECTS = new InjectionToken('Bootstrap Effects');
export function bootstrapEffects(effects, store) {
return () => {
merge(...effects.map(mergeEffects)).subscribe(store);
};
}
export function provideBootstrapEffect(effect: any) {
return [
effect,
{ provide: BOOTSTRAP_EFFECTS, multi: true, useExisting: effect },
];
}
@NgModule({
imports: [
EffectsModule.forRoot([])
],
providers: [
provideBootstrapEffect(SomeEffect)
{
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: bootstrapEffects,
deps: [[new Inject(BOOTSTRAP_EFFECTS)], Store]
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
This works for me. Though it's slightly more verbose than I hoped, at least I only have to set it up once for a single app.
Also, is there a way to make this work if BOOTSTRAP_EFFECTS is an array of effects? I've been trying to get that to work with no success. I'll try more when I get a free moment this week.
@jongunter here is an example that will take an array of effects
export const BOOTSTRAP_EFFECTS = new InjectionToken('Bootstrap Effects');
export function bootstrapEffects(effects, store) {
return () => {
merge(...effects.map(mergeEffects)).subscribe(store);
};
}
export function createInstances(...instances: any[]) {
return instances;
}
export function provideBootstrapEffects(effects: Type<any>[]) {
return [
effects,
{ provide: BOOTSTRAP_EFFECTS, deps: effects, useFactory: createInstances },
{
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: bootstrapEffects,
deps: [[new Inject(BOOTSTRAP_EFFECTS)], Store]
}
];
}
I keep getting ERROR TypeError: Actions must have a type property, using the code above whenever one of my effects emits any action. I've changed my bootstrapEffects method to make it work, but my hack cannot be the right way to do it. I feel like I'm missing something basic here.
export function bootstrapEffects(effects, store) {
return () => {
return merge(...effects.map(mergeEffects))
.map((effectEmission: any) => effectEmission.notification.value)
.filter(Boolean)
.subscribe(store);
};
}
Your way to fix the issue is correct. I was probably thinking about the old API. Here is a better example that doesn't require you to do that.
import {
NgModule,
APP_BOOTSTRAP_LISTENER,
InjectionToken,
Inject,
Type
} from '@angular/core';
import { Store } from '@ngrx/store';
import { EffectsModule, EffectSources } from '@ngrx/effects';
export const BOOTSTRAP_EFFECTS = new InjectionToken('Bootstrap Effects');
export function bootstrapEffects(effects: Type<any>[], sources: EffectSources) {
return () => {
effects.forEach(effect => sources.addEffects(effect));
};
}
export function createInstances(...instances: any[]) {
return instances;
}
export function provideBootstrapEffects(effects: Type<any>[]) {
return [
effects,
{ provide: BOOTSTRAP_EFFECTS, deps: effects, useFactory: createInstances },
{
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: bootstrapEffects,
deps: [[new Inject(BOOTSTRAP_EFFECTS)], EffectSources]
}
];
}
Thank you!
I followed the pattern above to get my feature effects to run after app init. That works, but the feature effects only receive actions from root-defined actions, not the feature actions.
Is there anything different I should do for a feature effect?
I'm trying to figure how to do the above as described by @brandonroberts but I'm getting an error TypeError: sources.addEffects is not a function. Any ideas. I'm running the 5.2.0 version of all @ngrx packages.
Getting the same issue as @jreilly-lukava :(
_Update:_
Think we figured it out, there's some slight copy typos/overlap between the original suggested code and the subsequent code updates posted.
TL;DR:
Replace this part of your ngModule providers section:
provideBootstrapEffect(SomeEffect)
{
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: bootstrapEffects,
deps: [[new Inject(BOOTSTRAP_EFFECTS)], Store]
}
with this:
provideBootstrapEffects([insert, your, array, of, effects, here]),
Full code, taken from the above posts and with the "sources.addEffects is not a function" error fixed:
import {
NgModule,
APP_BOOTSTRAP_LISTENER,
InjectionToken,
Inject,
Type
} from '@angular/core';
import { Store } from '@ngrx/store';
import { EffectsModule, EffectSources } from '@ngrx/effects';
export const BOOTSTRAP_EFFECTS = new InjectionToken('Bootstrap Effects');
export function bootstrapEffects(effects: Type<any>[], sources: EffectSources) {
return () => {
effects.forEach(effect => sources.addEffects(effect));
};
}
export function createInstances(...instances: any[]) {
return instances;
}
export function provideBootstrapEffects(effects: Type<any>[]) {
return [
effects,
{ provide: BOOTSTRAP_EFFECTS, deps: effects, useFactory: createInstances },
{
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: bootstrapEffects,
deps: [[new Inject(BOOTSTRAP_EFFECTS)], EffectSources]
}
];
}
@NgModule({
imports: [
EffectsModule.forRoot([])
],
providers: [
yourOtherStuff1,
provideBootstrapEffects([insert, your, array, of, effects, here]),
yourOtherStuff2
],
bootstrap: [AppComponent]
})
export class AppModule {}
Important, note the change of singular to plural for provideBootstrapEffect to provideBootstrapEffects
Any one still looking for help on this, I wrote a Medium for it and added a GitHub sample
https://medium.com/@remohy1/initialize-angular-app-with-ngrx-app-initializer-6556b819e0e3
https://github.com/mohyeid/ngrxInitializer
Actually, we still ended up with issues, so the above solution might not be 100%. I'll check out @mohyeid's page
FYI The issue we had is that ironically, APP_BOOTSTRAP_LISTENER was loading the effects too late.
So APP_INITIALIZER does it too soon, and APP_BOOTSTRAP_LISTENER did it too late. (Actions were already being fired from the store, before effects were loaded.
So, currently we're trying to solve this by creating our own InjectionToken to put in place of APP_BOOTSTRAP_LISTENER in the above code, and have this be triggered once our own configuration bootstrap is done. That way, effects load immediately after config is done. But having confusing times with the injection of this.
Still not solved this, so thoughts are welcome.
It seems this rabbit hole might be a little shorter if we just got EffectsModule.runAfterBootstrap back. It's understandable that it was removed initially because the framework itself didn't need to rely on that functionality but it seems there is a need for it in community.
@russelltrafford - I just ran into the same issue as you with the to early/to late deal. I'm interested to see how your team resolved it. Maybe there is a controlling actions similar to the controlling effects that @brandonroberts suggested? Something that can delay actions until after bootstrap?
Glad to see it's not just us. I made a new issue as this issue is actually closed and not 100% accurate to the actual remaining issue.
never resolved.. wow
Most helpful comment
It seems this rabbit hole might be a little shorter if we just got
EffectsModule.runAfterBootstrapback. It's understandable that it was removed initially because the framework itself didn't need to rely on that functionality but it seems there is a need for it in community.@russelltrafford - I just ran into the same issue as you with the to early/to late deal. I'm interested to see how your team resolved it. Maybe there is a controlling actions similar to the controlling effects that @brandonroberts suggested? Something that can delay actions until after bootstrap?