Skip to content

Commit 59ce3e2

Browse files
author
Leon Marzahn
authored
feat(effects): add user provided effects to EffectsModule.forFeature (#2231)
Closes #2232
1 parent 7598dc3 commit 59ce3e2

File tree

5 files changed

+164
-9
lines changed

5 files changed

+164
-9
lines changed

modules/effects/spec/integration.spec.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
OnIdentifyEffects,
1414
EffectSources,
1515
Actions,
16+
USER_PROVIDED_EFFECTS,
1617
} from '..';
1718
import { ofType, createEffect, OnRunEffects, EffectNotification } from '../src';
1819
import { mapTo, exhaustMap, tap } from 'rxjs/operators';
@@ -215,6 +216,60 @@ describe('NgRx Effects Integration spec', () => {
215216
// ngrxOnRunEffects should receive all actions except STORE_INIT
216217
expect(logger.actionsLog).toEqual(expectedLog.slice(1));
217218
});
219+
220+
it('should dispatch user provided effects actions in order', async () => {
221+
let dispatchedActionsLog: string[] = [];
222+
TestBed.resetTestingModule();
223+
TestBed.configureTestingModule({
224+
imports: [
225+
StoreModule.forRoot({
226+
dispatched: createDispatchedReducer(dispatchedActionsLog),
227+
}),
228+
EffectsModule.forRoot([
229+
EffectLoggerWithOnRunEffects,
230+
RootEffectWithInitAction,
231+
]),
232+
RouterTestingModule.withRoutes([]),
233+
],
234+
providers: [
235+
UserProvidedEffect1,
236+
{
237+
provide: USER_PROVIDED_EFFECTS,
238+
multi: true,
239+
useValue: [UserProvidedEffect1],
240+
},
241+
],
242+
});
243+
244+
const logger = TestBed.inject(EffectLoggerWithOnRunEffects);
245+
const router: Router = TestBed.inject(Router);
246+
const loader: SpyNgModuleFactoryLoader = TestBed.inject(
247+
NgModuleFactoryLoader
248+
) as SpyNgModuleFactoryLoader;
249+
250+
loader.stubbedModules = { feature: FeatModuleWithUserProvidedEffects };
251+
router.resetConfig([{ path: 'feature-path', loadChildren: 'feature' }]);
252+
253+
await router.navigateByUrl('/feature-path');
254+
255+
const expectedLog = [
256+
// Store init
257+
INIT,
258+
259+
// Root effects
260+
'[RootEffectWithInitAction]: INIT',
261+
262+
// User provided effects loaded by root module
263+
'[UserProvidedEffect1]: INIT',
264+
265+
// Effects init
266+
ROOT_EFFECTS_INIT,
267+
268+
// User provided effects loaded by feature module
269+
'[UserProvidedEffect2]: INIT',
270+
];
271+
expect(dispatchedActionsLog).toEqual(expectedLog);
272+
});
218273
});
219274

220275
@Injectable()
@@ -281,6 +336,31 @@ describe('NgRx Effects Integration spec', () => {
281336

282337
class RootEffectWithoutLifecycle {}
283338

339+
class UserProvidedEffect1 implements OnInitEffects {
340+
public ngrxOnInitEffects(): Action {
341+
return { type: '[UserProvidedEffect1]: INIT' };
342+
}
343+
}
344+
345+
class UserProvidedEffect2 implements OnInitEffects {
346+
public ngrxOnInitEffects(): Action {
347+
return { type: '[UserProvidedEffect2]: INIT' };
348+
}
349+
}
350+
351+
@NgModule({
352+
imports: [EffectsModule.forFeature()],
353+
providers: [
354+
UserProvidedEffect2,
355+
{
356+
provide: USER_PROVIDED_EFFECTS,
357+
multi: true,
358+
useValue: [UserProvidedEffect2],
359+
},
360+
],
361+
})
362+
class FeatModuleWithUserProvidedEffects {}
363+
284364
class FeatEffectWithInitAction implements OnInitEffects {
285365
ngrxOnInitEffects(): Action {
286366
return { type: '[FeatEffectWithInitAction]: INIT' };
@@ -307,7 +387,7 @@ describe('NgRx Effects Integration spec', () => {
307387
}
308388

309389
@NgModule({
310-
imports: [EffectsModule.forRoot([])],
390+
imports: [EffectsModule.forRoot()],
311391
})
312392
class FeatModuleWithForRoot {}
313393

modules/effects/src/effects_module.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Injector,
23
ModuleWithProviders,
34
NgModule,
45
Optional,
@@ -12,33 +13,46 @@ import { defaultEffectsErrorHandler } from './effects_error_handler';
1213
import { EffectsRootModule } from './effects_root_module';
1314
import { EffectsRunner } from './effects_runner';
1415
import {
16+
_FEATURE_EFFECTS,
17+
_ROOT_EFFECTS,
1518
_ROOT_EFFECTS_GUARD,
1619
EFFECTS_ERROR_HANDLER,
1720
FEATURE_EFFECTS,
1821
ROOT_EFFECTS,
22+
USER_PROVIDED_EFFECTS,
1923
} from './tokens';
2024

2125
@NgModule({})
2226
export class EffectsModule {
2327
static forFeature(
24-
featureEffects: Type<any>[]
28+
featureEffects: Type<any>[] = []
2529
): ModuleWithProviders<EffectsFeatureModule> {
2630
return {
2731
ngModule: EffectsFeatureModule,
2832
providers: [
2933
featureEffects,
34+
{
35+
provide: _FEATURE_EFFECTS,
36+
multi: true,
37+
useValue: featureEffects,
38+
},
39+
{
40+
provide: USER_PROVIDED_EFFECTS,
41+
multi: true,
42+
useValue: [],
43+
},
3044
{
3145
provide: FEATURE_EFFECTS,
3246
multi: true,
33-
deps: featureEffects,
34-
useFactory: createSourceInstances,
47+
useFactory: createEffects,
48+
deps: [Injector, _FEATURE_EFFECTS, USER_PROVIDED_EFFECTS],
3549
},
3650
],
3751
};
3852
}
3953

4054
static forRoot(
41-
rootEffects: Type<any>[]
55+
rootEffects: Type<any>[] = []
4256
): ModuleWithProviders<EffectsRootModule> {
4357
return {
4458
ngModule: EffectsRootModule,
@@ -56,18 +70,48 @@ export class EffectsModule {
5670
EffectSources,
5771
Actions,
5872
rootEffects,
73+
{
74+
provide: _ROOT_EFFECTS,
75+
useValue: [rootEffects],
76+
},
77+
{
78+
provide: USER_PROVIDED_EFFECTS,
79+
multi: true,
80+
useValue: [],
81+
},
5982
{
6083
provide: ROOT_EFFECTS,
61-
deps: rootEffects,
62-
useFactory: createSourceInstances,
84+
useFactory: createEffects,
85+
deps: [Injector, _ROOT_EFFECTS, USER_PROVIDED_EFFECTS],
6386
},
6487
],
6588
};
6689
}
6790
}
6891

69-
export function createSourceInstances(...instances: any[]) {
70-
return instances;
92+
export function createEffects(
93+
injector: Injector,
94+
effectGroups: Type<any>[][],
95+
userProvidedEffectGroups: Type<any>[][]
96+
): any[] {
97+
const mergedEffects: Type<any>[] = [];
98+
99+
for (let effectGroup of effectGroups) {
100+
mergedEffects.push(...effectGroup);
101+
}
102+
103+
for (let userProvidedEffectGroup of userProvidedEffectGroups) {
104+
mergedEffects.push(...userProvidedEffectGroup);
105+
}
106+
107+
return createEffectInstances(injector, mergedEffects);
108+
}
109+
110+
export function createEffectInstances(
111+
injector: Injector,
112+
effects: Type<any>[]
113+
): any[] {
114+
return effects.map(effect => injector.get(effect));
71115
}
72116

73117
export function _provideForRootGuard(runner: EffectsRunner): any {

modules/effects/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export {
2626
OnRunEffects,
2727
OnInitEffects,
2828
} from './lifecycle_hooks';
29+
export { USER_PROVIDED_EFFECTS } from './tokens';

modules/effects/src/tokens.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,18 @@ export const _ROOT_EFFECTS_GUARD = new InjectionToken<void>(
77
export const IMMEDIATE_EFFECTS = new InjectionToken<any[]>(
88
'ngrx/effects: Immediate Effects'
99
);
10+
export const USER_PROVIDED_EFFECTS = new InjectionToken<Type<any>[][]>(
11+
'ngrx/effects: User Provided Effects'
12+
);
13+
export const _ROOT_EFFECTS = new InjectionToken<Type<any>[]>(
14+
'ngrx/effects: Internal Root Effects'
15+
);
1016
export const ROOT_EFFECTS = new InjectionToken<Type<any>[]>(
1117
'ngrx/effects: Root Effects'
1218
);
19+
export const _FEATURE_EFFECTS = new InjectionToken<Type<any>[]>(
20+
'ngrx/effects: Internal Feature Effects'
21+
);
1322
export const FEATURE_EFFECTS = new InjectionToken<any[][]>(
1423
'ngrx/effects: Feature Effects'
1524
);

projects/ngrx.io/content/guide/effects/index.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,27 @@ export class MovieModule {}
225225

226226
</div>
227227

228+
## Alternative way of registering effects
229+
230+
You can provide root-/feature-level effects with the provider `USER_PROVIDED_EFFECTS`.
231+
232+
<code-example header="movies.module.ts">
233+
providers: [
234+
MovieEffects,
235+
{
236+
provide: USER_PROVIDED_EFFECTS,
237+
multi: true,
238+
useValue: [MovieEffects],
239+
},
240+
]
241+
</code-example>
242+
243+
<div class="alert is-critical">
244+
245+
The `EffectsModule.forFeature()` method must be added to the module imports even if you only provide effects over token, and don't pass them via parameters. (Same goes for `EffectsModule.forRoot()`)
246+
247+
</div>
248+
228249
## Incorporating State
229250

230251
If additional metadata is needed to perform an effect besides the initiating action's `type`, we should rely on passed metadata from an action creator's `props` method.

0 commit comments

Comments
 (0)