Skip to content

Commit a528320

Browse files
timdeschryverbrandonroberts
authored andcommitted
fix(effects): dispatch init action once (#2164)
Closes #2106 BREAKING CHANGE: BEFORE: When the effect class was registered, the init action would be dispatched. If the effect was provided in multiple lazy loaded modules, the init action would be dispatched for every module. AFTER: The init action is only dispatched once The init action is now dispatched based on the identifier of the effect (via ngrxOnIdentifyEffects)
1 parent 714ae5f commit a528320

File tree

3 files changed

+100
-16
lines changed

3 files changed

+100
-16
lines changed

modules/effects/spec/effect_sources.spec.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import { ErrorHandler } from '@angular/core';
22
import { TestBed } from '@angular/core/testing';
33
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
4-
import { concat, NEVER, Observable, of, throwError, timer } from 'rxjs';
5-
import { concatMap, map } from 'rxjs/operators';
4+
import {
5+
concat,
6+
NEVER,
7+
Observable,
8+
of,
9+
throwError,
10+
timer,
11+
Subject,
12+
} from 'rxjs';
13+
import { map, mapTo } from 'rxjs/operators';
614

715
import {
816
Effect,
917
EffectSources,
1018
OnIdentifyEffects,
1119
OnInitEffects,
1220
createEffect,
21+
Actions,
1322
} from '../';
23+
import { EffectsRunner } from '../src/effects_runner';
1424
import { Store } from '@ngrx/store';
25+
import { ofType } from '../src';
1526

1627
describe('EffectSources', () => {
1728
let mockErrorReporter: ErrorHandler;
@@ -21,6 +32,7 @@ describe('EffectSources', () => {
2132
TestBed.configureTestingModule({
2233
providers: [
2334
EffectSources,
35+
EffectsRunner,
2436
{
2537
provide: Store,
2638
useValue: {
@@ -30,6 +42,9 @@ describe('EffectSources', () => {
3042
],
3143
});
3244

45+
const effectsRunner = TestBed.get(EffectsRunner);
46+
effectsRunner.start();
47+
3348
mockErrorReporter = TestBed.get(ErrorHandler);
3449
effectSources = TestBed.get(EffectSources);
3550

@@ -51,15 +66,83 @@ describe('EffectSources', () => {
5166
return { type: '[EffectWithInitAction] Init' };
5267
}
5368
}
69+
const store = TestBed.get(Store);
5470

5571
effectSources.addEffects(new EffectWithInitAction());
5672

73+
expect(store.dispatch).toHaveBeenCalledTimes(1);
74+
expect(store.dispatch).toHaveBeenCalledWith({
75+
type: '[EffectWithInitAction] Init',
76+
});
77+
});
78+
79+
it('should dispatch an action on ngrxOnInitEffects after being registered (class has effects)', () => {
80+
class EffectWithInitActionAndEffects implements OnInitEffects {
81+
effectOne = createEffect(() => {
82+
return this.actions$.pipe(
83+
ofType('Action 1'),
84+
mapTo({ type: 'Action 1 Response' })
85+
);
86+
});
87+
effectTwo = createEffect(() => {
88+
return this.actions$.pipe(
89+
ofType('Action 2'),
90+
mapTo({ type: 'Action 2 Response' })
91+
);
92+
});
93+
94+
ngrxOnInitEffects() {
95+
return { type: '[EffectWithInitAction] Init' };
96+
}
97+
98+
constructor(private actions$: Actions) {}
99+
}
57100
const store = TestBed.get(Store);
101+
102+
effectSources.addEffects(new EffectWithInitActionAndEffects(new Subject()));
103+
104+
expect(store.dispatch).toHaveBeenCalledTimes(1);
58105
expect(store.dispatch).toHaveBeenCalledWith({
59106
type: '[EffectWithInitAction] Init',
60107
});
61108
});
62109

110+
it('should only dispatch an action on ngrxOnInitEffects once after being registered', () => {
111+
class EffectWithInitAction implements OnInitEffects {
112+
ngrxOnInitEffects() {
113+
return { type: '[EffectWithInitAction] Init' };
114+
}
115+
}
116+
const store = TestBed.get(Store);
117+
118+
effectSources.addEffects(new EffectWithInitAction());
119+
effectSources.addEffects(new EffectWithInitAction());
120+
121+
expect(store.dispatch).toHaveBeenCalledTimes(1);
122+
});
123+
124+
it('should dispatch an action on ngrxOnInitEffects multiple times after being registered with different identifiers', () => {
125+
let id = 0;
126+
class EffectWithInitAction implements OnInitEffects, OnIdentifyEffects {
127+
effectId = '';
128+
ngrxOnIdentifyEffects(): string {
129+
return this.effectId;
130+
}
131+
ngrxOnInitEffects() {
132+
return { type: '[EffectWithInitAction] Init' };
133+
}
134+
constructor() {
135+
this.effectId = (id++).toString();
136+
}
137+
}
138+
const store = TestBed.get(Store);
139+
140+
effectSources.addEffects(new EffectWithInitAction());
141+
effectSources.addEffects(new EffectWithInitAction());
142+
143+
expect(store.dispatch).toHaveBeenCalledTimes(2);
144+
});
145+
63146
describe('toActions() Operator', () => {
64147
describe('with @Effect()', () => {
65148
const a = { type: 'From Source A' };

modules/effects/spec/integration.spec.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('NgRx Effects Integration spec', () => {
4949
});
5050

5151
it('should dispatch init actions in the correct order', () => {
52-
expect(dispatch.calls.count()).toBe(7);
52+
expect(dispatch.calls.count()).toBe(6);
5353

5454
// All of the root effects init actions are dispatched first
5555
expect(dispatch.calls.argsFor(0)).toEqual([
@@ -73,11 +73,6 @@ describe('NgRx Effects Integration spec', () => {
7373
expect(dispatch.calls.argsFor(5)).toEqual([
7474
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
7575
]);
76-
77-
// While the effect has the same identifier the init effect action is still being dispatched
78-
expect(dispatch.calls.argsFor(6)).toEqual([
79-
{ type: '[FeatEffectWithIdentifierAndInitAction]: INIT' },
80-
]);
8176
});
8277

8378
it('throws if forRoot() is used more than once', (done: DoneFn) => {

modules/effects/src/effect_sources.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
groupBy,
99
map,
1010
mergeMap,
11+
tap,
1112
} from 'rxjs/operators';
1213

1314
import {
@@ -31,13 +32,6 @@ export class EffectSources extends Subject<any> {
3132

3233
addEffects(effectSourceInstance: any): void {
3334
this.next(effectSourceInstance);
34-
35-
if (
36-
onInitEffects in effectSourceInstance &&
37-
typeof effectSourceInstance[onInitEffects] === 'function'
38-
) {
39-
this.store.dispatch(effectSourceInstance[onInitEffects]());
40-
}
4135
}
4236

4337
/**
@@ -46,7 +40,19 @@ export class EffectSources extends Subject<any> {
4640
toActions(): Observable<Action> {
4741
return this.pipe(
4842
groupBy(getSourceForInstance),
49-
mergeMap(source$ => source$.pipe(groupBy(effectsInstance))),
43+
mergeMap(source$ => {
44+
return source$.pipe(
45+
groupBy(effectsInstance),
46+
tap(() => {
47+
if (
48+
onInitEffects in source$.key &&
49+
typeof source$.key[onInitEffects] === 'function'
50+
) {
51+
this.store.dispatch(source$.key.ngrxOnInitEffects());
52+
}
53+
})
54+
);
55+
}),
5056
mergeMap(source$ =>
5157
source$.pipe(
5258
exhaustMap(resolveEffectSource(this.errorHandler)),

0 commit comments

Comments
 (0)