Skip to content

Commit

Permalink
feat(effects): remove @effect decorator (#3634)
Browse files Browse the repository at this point in the history
Closes #3617 

BREAKING CHANGES:

The @effect decorator is removed

BEFORE:

Defining an effect is done with @effect

@effect()
data$ = this.actions$.pipe();

AFTER:

Defining an effect is done with createEffect

data$ = createEffect(() => this.actions$.pipe());
  • Loading branch information
timdeschryver authored Oct 28, 2022
1 parent b9c1ab6 commit 96c5bdd
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 467 deletions.
45 changes: 0 additions & 45 deletions modules/effects/spec/effect_decorator.spec.ts

This file was deleted.

246 changes: 0 additions & 246 deletions modules/effects/spec/effect_sources.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
import { map } from 'rxjs/operators';

import {
Effect,
EffectSources,
OnIdentifyEffects,
OnInitEffects,
Expand Down Expand Up @@ -76,251 +75,6 @@ describe('EffectSources', () => {
return (effectSources as any)['toActions'].call(source);
}

describe('with @Effect()', () => {
const a = { type: 'From Source A' };
const b = { type: 'From Source B' };
const c = { type: 'From Source C that completes' };
const d = { not: 'a valid action' };
const e = undefined;
const f = null;
const i = { type: 'From Source Identifier' };
const i2 = { type: 'From Source Identifier 2' };

const circularRef = {} as any;
circularRef.circularRef = circularRef;
const g = { circularRef };

const error = new Error('An Error');

class SourceA {
@Effect() a$ = alwaysOf(a);
}

class SourceB {
@Effect() b$ = alwaysOf(b);
}

class SourceC {
@Effect() c$ = of(c);
}

class SourceD {
@Effect() d$ = alwaysOf(d);
}

class SourceE {
@Effect() e$ = alwaysOf(e);
}

class SourceF {
@Effect() f$ = alwaysOf(f);
}

class SourceG {
@Effect() g$ = alwaysOf(g);
}

class SourceError {
@Effect() e$ = throwError(() => error);
}

class SourceH {
@Effect() empty = of('value');
@Effect()
never = timer(50, getTestScheduler() as any).pipe(map(() => 'update'));
}

class SourceWithIdentifier implements OnIdentifyEffects {
effectIdentifier: string;
@Effect() i$ = alwaysOf(i);

ngrxOnIdentifyEffects() {
return this.effectIdentifier;
}

constructor(identifier: string) {
this.effectIdentifier = identifier;
}
}

class SourceWithIdentifier2 implements OnIdentifyEffects {
effectIdentifier: string;
@Effect() i2$ = alwaysOf(i2);

ngrxOnIdentifyEffects() {
return this.effectIdentifier;
}

constructor(identifier: string) {
this.effectIdentifier = identifier;
}
}

it('should resolve effects from instances', () => {
const sources$ = cold('--a--', { a: new SourceA() });
const expected = cold('--a--', { a });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should ignore duplicate sources', () => {
const sources$ = cold('--a--a--a--', {
a: new SourceA(),
});
const expected = cold('--a--------', { a });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should resolve effects with different identifiers', () => {
const sources$ = cold('--a--b--c--', {
a: new SourceWithIdentifier('a'),
b: new SourceWithIdentifier('b'),
c: new SourceWithIdentifier('c'),
});
const expected = cold('--i--i--i--', { i });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should ignore effects with the same identifier', () => {
const sources$ = cold('--a--b--c--', {
a: new SourceWithIdentifier('a'),
b: new SourceWithIdentifier('a'),
c: new SourceWithIdentifier('a'),
});
const expected = cold('--i--------', { i });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should resolve effects with same identifiers but different classes', () => {
const sources$ = cold('--a--b--c--d--', {
a: new SourceWithIdentifier('a'),
b: new SourceWithIdentifier2('a'),
c: new SourceWithIdentifier('b'),
d: new SourceWithIdentifier2('b'),
});
const expected = cold('--a--b--a--b--', { a: i, b: i2 });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});

it('should report an error if an effect dispatches an invalid action', () => {
const sources$ = of(new SourceD());

toActions(sources$).subscribe();

expect(mockErrorReporter.handleError).toHaveBeenCalledWith(
new Error(
'Effect "SourceD.d$" dispatched an invalid action: {"not":"a valid action"}'
)
);
});

it('should report an error if an effect dispatches an `undefined`', () => {
const sources$ = of(new SourceE());

toActions(sources$).subscribe();

expect(mockErrorReporter.handleError).toHaveBeenCalledWith(
new Error(
'Effect "SourceE.e$" dispatched an invalid action: undefined'
)
);
});

it('should report an error if an effect dispatches a `null`', () => {
const sources$ = of(new SourceF());

toActions(sources$).subscribe();

expect(mockErrorReporter.handleError).toHaveBeenCalledWith(
new Error('Effect "SourceF.f$" dispatched an invalid action: null')
);
});

it('should report an error if an effect throws one', () => {
const sources$ = of(new SourceError());

toActions(sources$).subscribe();

expect(mockErrorReporter.handleError).toHaveBeenCalledWith(
new Error('An Error')
);
});

it('should resubscribe on error by default', () => {
class Eff {
@Effect()
b$ = hot('a--e--b--e--c--e--d').pipe(
map((v) => {
if (v == 'e') throw new Error('An Error');
return v;
})
);
}

const sources$ = of(new Eff());

// 👇 'e' is ignored.
const expected = cold('a-----b-----c-----d');
expect(toActions(sources$)).toBeObservable(expected);
});

it('should not resubscribe on error when useEffectsErrorHandler is false', () => {
class Eff {
@Effect({ useEffectsErrorHandler: false })
b$ = hot('a--b--c--d').pipe(
map((v) => {
if (v == 'b') throw new Error('An Error');
return v;
})
);
}

const sources$ = of(new Eff());

// 👇 completes.
const expected = cold('a--|');

expect(toActions(sources$)).toBeObservable(expected);
});

it(`should not break when the action in the error message can't be stringified`, () => {
const sources$ = of(new SourceG());

toActions(sources$).subscribe();

expect(mockErrorReporter.handleError).toHaveBeenCalledWith(
new Error(
'Effect "SourceG.g$" dispatched an invalid action: [object Object]'
)
);
});

it('should not complete the group if just one effect completes', () => {
const sources$ = cold('g', {
g: new SourceH(),
});
const expected = cold('a----b-----', { a: 'value', b: 'update' });

const output = toActions(sources$);

expect(output).toBeObservable(expected);
});
});

describe('with createEffect()', () => {
const a = { type: 'From Source A' };
const b = { type: 'From Source B' };
Expand Down
25 changes: 1 addition & 24 deletions modules/effects/spec/effects_feature_module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
StoreModule,
} from '@ngrx/store';
import { map, withLatestFrom } from 'rxjs/operators';
import { Actions, Effect, EffectsModule, ofType, createEffect } from '../';
import { Actions, EffectsModule, ofType, createEffect } from '../';
import { EffectsFeatureModule } from '../src/effects_feature_module';
import { EffectsRootModule } from '../src/effects_root_module';
import { FEATURE_EFFECTS } from '../src/tokens';
Expand Down Expand Up @@ -67,22 +67,6 @@ describe('Effects Feature Module', () => {
store = TestBed.inject(Store);
});

it('should have the feature state defined to select from the effect', (done: any) => {
const action = { type: 'INCREMENT' };
const result = { type: 'INCREASE' };

effects.effectWithStore.subscribe((res) => {
expect(res).toEqual(result);
});

store.dispatch(action);

store.pipe(select(getDataState)).subscribe((data) => {
expect(data).toBe(110);
done();
});
});

it('should have the feature state defined to select from the createEffect', (done: any) => {
const action = { type: 'CREATE_INCREMENT' };
const result = { type: 'CREATE_INCREASE' };
Expand Down Expand Up @@ -145,13 +129,6 @@ const getCreateDataState = createSelector(
class FeatureEffects {
constructor(private actions: Actions, private store: Store<State>) {}

@Effect()
effectWithStore = this.actions.pipe(
ofType('INCREMENT'),
withLatestFrom(this.store.select(getDataState)),
map(([action, state]) => ({ type: 'INCREASE' }))
);

createEffectWithStore = createEffect(() =>
this.actions.pipe(
ofType('CREATE_INCREMENT'),
Expand Down
Loading

0 comments on commit 96c5bdd

Please sign in to comment.