Skip to content

Commit 04e07a6

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(effects): allow non-dispatching effects to not return an action (#1689)
1 parent d472757 commit 04e07a6

File tree

5 files changed

+148
-84
lines changed

5 files changed

+148
-84
lines changed

modules/effects/spec/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_test_library(
1212
"//modules/effects",
1313
"//modules/store",
1414
"@npm//rxjs",
15+
"@npm//ts-snippet",
1516
],
1617
)
1718

modules/effects/spec/create_effect.spec.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { of } from 'rxjs';
2+
import { expecter } from 'ts-snippet';
3+
import { createEffect, getCreateEffectMetadata } from '../src/effect_creator';
4+
5+
describe('createEffect()', () => {
6+
describe('types', () => {
7+
const expectSnippet = expecter(
8+
code => `
9+
import { Action } from '@ngrx/store';
10+
import { createEffect } from '@ngrx/effects';
11+
import { of } from 'rxjs';
12+
${code}`,
13+
{
14+
moduleResolution: 'node',
15+
target: 'es2015',
16+
baseUrl: '.',
17+
experimentalDecorators: true,
18+
paths: {
19+
'@ngrx/store': ['./modules/store'],
20+
'@ngrx/effects': ['./modules/effects'],
21+
rxjs: ['../npm/node_modules/rxjs', './node_modules/rxjs'],
22+
},
23+
}
24+
);
25+
26+
describe('dispatch: true', () => {
27+
it('should enforce an Action return value', () => {
28+
expectSnippet(`
29+
const effect = createEffect(() => of({ type: 'a' }));
30+
`).toSucceed();
31+
32+
expectSnippet(`
33+
const effect = createEffect(() => of({ foo: 'a' }));
34+
`).toFail(
35+
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
36+
);
37+
});
38+
39+
it('should enforce an Action return value when dispatch is provided', () => {
40+
expectSnippet(`
41+
const effect = createEffect(() => of({ type: 'a' }), { dispatch: true });
42+
`).toSucceed();
43+
44+
expectSnippet(`
45+
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: true });
46+
`).toFail(
47+
/Type 'Observable<{ foo: string; }>' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
48+
);
49+
});
50+
});
51+
52+
describe('dispatch: false', () => {
53+
it('should enforce an Observable return value', () => {
54+
expectSnippet(`
55+
const effect = createEffect(() => of({ foo: 'a' }), { dispatch: false });
56+
`).toSucceed();
57+
58+
expectSnippet(`
59+
const effect = createEffect(() => ({ foo: 'a' }), { dispatch: false });
60+
`).toFail(
61+
/Type '{ foo: string; }' is not assignable to type 'Observable<Action> | ((...args: any[]) => Observable<Action>)'./
62+
);
63+
});
64+
});
65+
});
66+
67+
it('should flag the variable with a meta tag', () => {
68+
const effect = createEffect(() => of({ type: 'a' }));
69+
70+
expect(effect.hasOwnProperty('__@ngrx/effects_create__')).toBe(true);
71+
});
72+
73+
it('should dispatch by default', () => {
74+
const effect: any = createEffect(() => of({ type: 'a' }));
75+
76+
expect(effect['__@ngrx/effects_create__']).toEqual({ dispatch: true });
77+
});
78+
79+
it('should be possible to explicitly create a dispatching effect', () => {
80+
const effect: any = createEffect(() => of({ type: 'a' }), {
81+
dispatch: true,
82+
});
83+
84+
expect(effect['__@ngrx/effects_create__']).toEqual({ dispatch: true });
85+
});
86+
87+
it('should be possible to create a non-dispatching effect', () => {
88+
const effect: any = createEffect(() => of({ type: 'a' }), {
89+
dispatch: false,
90+
});
91+
92+
expect(effect['__@ngrx/effects_create__']).toEqual({ dispatch: false });
93+
});
94+
95+
it('should be possible to create a non-dispatching effect returning a non-action', () => {
96+
const effect: any = createEffect(() => of('foo'), {
97+
dispatch: false,
98+
});
99+
100+
expect(effect['__@ngrx/effects_create__']).toEqual({ dispatch: false });
101+
});
102+
103+
describe('getCreateEffectMetadata', () => {
104+
it('should get the effects metadata for a class instance', () => {
105+
class Fixture {
106+
a = createEffect(() => of({ type: 'a' }));
107+
b = createEffect(() => of({ type: 'b' }), { dispatch: true });
108+
c = createEffect(() => of({ type: 'c' }), { dispatch: false });
109+
}
110+
111+
const mock = new Fixture();
112+
113+
expect(getCreateEffectMetadata(mock)).toEqual([
114+
{ propertyName: 'a', dispatch: true },
115+
{ propertyName: 'b', dispatch: true },
116+
{ propertyName: 'c', dispatch: false },
117+
]);
118+
});
119+
120+
it('should return an empty array if the effect has not been created with createEffect()', () => {
121+
const fakeCreateEffect: any = () => {};
122+
class Fixture {
123+
a = fakeCreateEffect(() => of({ type: 'A' }));
124+
}
125+
126+
const mock = new Fixture();
127+
128+
expect(getCreateEffectMetadata(mock)).toEqual([]);
129+
});
130+
});
131+
});

modules/effects/src/effect_creator.ts

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,17 @@ import { EffectMetadata } from './models';
44

55
const CREATE_EFFECT_METADATA_KEY = '__@ngrx/effects_create__';
66

7-
export function createEffect<T extends Action>(
8-
source: (() => Observable<T>),
9-
options: { dispatch: false }
10-
): Observable<T>;
11-
export function createEffect<T extends Action>(
12-
source: (() => (...args: any[]) => Observable<T>),
13-
options: { dispatch: false }
14-
): ((...args: any[]) => Observable<T>);
15-
export function createEffect<T extends Action>(
16-
source: (() => Observable<T>),
17-
options?: { dispatch: true }
18-
): Observable<T>;
19-
export function createEffect<T extends Action>(
20-
source: (() => (...args: any[]) => Observable<T>),
21-
options?: { dispatch: true }
22-
): ((...args: any[]) => Observable<T>);
23-
export function createEffect<T extends Action>(
24-
source: (() => Observable<T>) | (() => (...args: any[]) => Observable<T>),
25-
{ dispatch = true } = {}
26-
): Observable<T> | ((...args: any[]) => Observable<T>) {
7+
export function createEffect<
8+
R extends Observable<unknown> | ((...args: any[]) => Observable<unknown>)
9+
>(source: () => R, options: { dispatch: false }): R;
10+
export function createEffect<
11+
T extends Action,
12+
R extends Observable<T> | ((...args: any[]) => Observable<T>)
13+
>(source: () => R, options?: { dispatch: true }): R;
14+
export function createEffect<
15+
T extends Action,
16+
R extends Observable<T> | ((...args: any[]) => Observable<T>)
17+
>(source: () => R, { dispatch = true } = {}): R {
2718
const effect = source();
2819
Object.defineProperty(effect, CREATE_EFFECT_METADATA_KEY, {
2920
value: {

projects/example-app/src/app/books/effects/collection.effects.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ export class CollectionEffects {
1818
*
1919
* The `defer` observable accepts an observable factory function
2020
* that is called when the observable is subscribed to.
21-
* Wrapping the database open call in `defer` makes
21+
* Wrapping the supported call in `defer` makes
2222
* effect easier to test.
2323
*/
24-
@Effect({ dispatch: false })
25-
checkStorageSupport$ = defer(() => this.storageService.supported());
24+
checkStorageSupport$ = createEffect(
25+
() => defer(() => this.storageService.supported()),
26+
{ dispatch: false }
27+
);
2628

2729
loadCollection$ = createEffect(() =>
2830
this.actions$.pipe(

0 commit comments

Comments
 (0)