-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(effects): add provideEffects function (#3524)
Closes #3522
- Loading branch information
1 parent
6b0db4e
commit db35bfe
Showing
11 changed files
with
285 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import { ENVIRONMENT_INITIALIZER, inject, Injectable } from '@angular/core'; | ||
import { TestBed } from '@angular/core/testing'; | ||
import { map } from 'rxjs'; | ||
import { | ||
createAction, | ||
createFeatureSelector, | ||
createReducer, | ||
props, | ||
provideState, | ||
provideStore, | ||
Store, | ||
} from '@ngrx/store'; | ||
import { | ||
Actions, | ||
concatLatestFrom, | ||
createEffect, | ||
EffectsRunner, | ||
ofType, | ||
provideEffects, | ||
rootEffectsInit, | ||
} from '../src/index'; | ||
|
||
describe('provideEffects', () => { | ||
it('starts effects runner when called first time', () => { | ||
TestBed.configureTestingModule({ | ||
providers: [ | ||
{ | ||
provide: ENVIRONMENT_INITIALIZER, | ||
multi: true, | ||
useValue: () => jest.spyOn(inject(EffectsRunner), 'start'), | ||
}, | ||
provideStore({}).ɵproviders, | ||
// provide effects twice | ||
provideEffects([]).ɵproviders, | ||
provideEffects([]).ɵproviders, | ||
], | ||
}); | ||
|
||
const effectsRunner = TestBed.inject(EffectsRunner); | ||
expect(effectsRunner.start).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('dispatches effects init action when called first time', () => { | ||
TestBed.configureTestingModule({ | ||
providers: [ | ||
{ | ||
provide: ENVIRONMENT_INITIALIZER, | ||
multi: true, | ||
useValue: () => jest.spyOn(inject(Store), 'dispatch'), | ||
}, | ||
provideStore().ɵproviders, | ||
// provide effects twice | ||
provideEffects([]).ɵproviders, | ||
provideEffects([]).ɵproviders, | ||
], | ||
}); | ||
|
||
const store = TestBed.inject(Store); | ||
expect(store.dispatch).toHaveBeenCalledWith(rootEffectsInit()); | ||
expect(store.dispatch).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('throws an error when store is not provided', () => { | ||
TestBed.configureTestingModule({ | ||
// provide only effects | ||
providers: [provideEffects([TestEffects]).ɵproviders], | ||
}); | ||
|
||
expect(() => TestBed.inject(TestEffects)).toThrowError(); | ||
}); | ||
|
||
it('runs provided effects', (done) => { | ||
TestBed.configureTestingModule({ | ||
providers: [ | ||
provideStore().ɵproviders, | ||
provideEffects([TestEffects]).ɵproviders, | ||
], | ||
}); | ||
|
||
const store = TestBed.inject(Store); | ||
const effects = TestBed.inject(TestEffects); | ||
|
||
effects.simpleEffect$.subscribe((action) => { | ||
expect(action).toEqual(simpleEffectDone()); | ||
done(); | ||
}); | ||
|
||
store.dispatch(simpleEffectTest()); | ||
}); | ||
|
||
it('runs provided effects after root state registration', (done) => { | ||
TestBed.configureTestingModule({ | ||
providers: [ | ||
provideEffects([TestEffects]).ɵproviders, | ||
// provide store after effects | ||
provideStore({ [rootSliceKey]: createReducer('ngrx') }).ɵproviders, | ||
], | ||
}); | ||
|
||
const store = TestBed.inject(Store); | ||
const effects = TestBed.inject(TestEffects); | ||
|
||
effects.effectWithRootState$.subscribe((action) => { | ||
expect(action).toEqual( | ||
effectWithRootStateDone({ [rootSliceKey]: 'ngrx' }) | ||
); | ||
done(); | ||
}); | ||
|
||
store.dispatch(effectWithRootStateTest()); | ||
}); | ||
|
||
it('runs provided effects after feature state registration', (done) => { | ||
TestBed.configureTestingModule({ | ||
providers: [ | ||
provideStore().ɵproviders, | ||
provideEffects([TestEffects]).ɵproviders, | ||
// provide feature state after effects | ||
provideState(featureSliceKey, createReducer('effects')).ɵproviders, | ||
], | ||
}); | ||
|
||
const store = TestBed.inject(Store); | ||
const effects = TestBed.inject(TestEffects); | ||
|
||
effects.effectWithFeatureState$.subscribe((action) => { | ||
expect(action).toEqual( | ||
effectWithFeatureStateDone({ [featureSliceKey]: 'effects' }) | ||
); | ||
done(); | ||
}); | ||
|
||
store.dispatch(effectWithFeatureStateTest()); | ||
}); | ||
}); | ||
|
||
const rootSliceKey = 'rootSlice'; | ||
const featureSliceKey = 'featureSlice'; | ||
const selectRootSlice = createFeatureSelector<string>(rootSliceKey); | ||
const selectFeatureSlice = createFeatureSelector<string>(featureSliceKey); | ||
|
||
const simpleEffectTest = createAction('simpleEffectTest'); | ||
const simpleEffectDone = createAction('simpleEffectDone'); | ||
const effectWithRootStateTest = createAction('effectWithRootStateTest'); | ||
const effectWithRootStateDone = createAction( | ||
'effectWithRootStateDone', | ||
props<{ [rootSliceKey]: string }>() | ||
); | ||
const effectWithFeatureStateTest = createAction('effectWithFeatureStateTest'); | ||
const effectWithFeatureStateDone = createAction( | ||
'effectWithFeatureStateDone', | ||
props<{ [featureSliceKey]: string }>() | ||
); | ||
|
||
@Injectable() | ||
class TestEffects { | ||
constructor( | ||
private readonly actions$: Actions, | ||
private readonly store: Store | ||
) {} | ||
|
||
readonly simpleEffect$ = createEffect(() => { | ||
return this.actions$.pipe( | ||
ofType(simpleEffectTest), | ||
map(() => simpleEffectDone()) | ||
); | ||
}); | ||
|
||
readonly effectWithRootState$ = createEffect(() => { | ||
return this.actions$.pipe( | ||
ofType(effectWithRootStateTest), | ||
concatLatestFrom(() => this.store.select(selectRootSlice)), | ||
map(([, rootSlice]) => effectWithRootStateDone({ rootSlice })) | ||
); | ||
}); | ||
|
||
readonly effectWithFeatureState$ = createEffect(() => { | ||
return this.actions$.pipe( | ||
ofType(effectWithFeatureStateTest), | ||
concatLatestFrom(() => this.store.select(selectFeatureSlice)), | ||
map(([, featureSlice]) => effectWithFeatureStateDone({ featureSlice })) | ||
); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { createAction } from '@ngrx/store'; | ||
|
||
export const ROOT_EFFECTS_INIT = '@ngrx/effects/init'; | ||
export const rootEffectsInit = createAction(ROOT_EFFECTS_INIT); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { | ||
ENVIRONMENT_INITIALIZER, | ||
inject, | ||
InjectFlags, | ||
Type, | ||
} from '@angular/core'; | ||
import { | ||
EnvironmentProviders, | ||
FEATURE_STATE_PROVIDER, | ||
ROOT_STORE_PROVIDER, | ||
Store, | ||
} from '@ngrx/store'; | ||
import { EffectsRunner } from './effects_runner'; | ||
import { EffectSources } from './effect_sources'; | ||
import { rootEffectsInit as effectsInit } from './effects_actions'; | ||
|
||
/** | ||
* Runs the provided effects. | ||
* Can be called at the root and feature levels. | ||
* | ||
* @usageNotes | ||
* | ||
* ### Providing effects at the root level | ||
* | ||
* ```ts | ||
* bootstrapApplication(AppComponent, { | ||
* providers: [provideEffects([RouterEffects])], | ||
* }); | ||
* ``` | ||
* | ||
* ### Providing effects at the feature level | ||
* | ||
* ```ts | ||
* const booksRoutes: Route[] = [ | ||
* { | ||
* path: '', | ||
* providers: [provideEffects([BooksApiEffects])], | ||
* children: [ | ||
* { path: '', component: BookListComponent }, | ||
* { path: ':id', component: BookDetailsComponent }, | ||
* ], | ||
* }, | ||
* ]; | ||
* ``` | ||
*/ | ||
export function provideEffects(effects: Type<unknown>[]): EnvironmentProviders { | ||
return { | ||
ɵproviders: [ | ||
effects, | ||
{ | ||
provide: ENVIRONMENT_INITIALIZER, | ||
multi: true, | ||
useValue: () => { | ||
inject(ROOT_STORE_PROVIDER); | ||
inject(FEATURE_STATE_PROVIDER, InjectFlags.Optional); | ||
|
||
const effectsRunner = inject(EffectsRunner); | ||
const effectSources = inject(EffectSources); | ||
const shouldInitEffects = !effectsRunner.isStarted; | ||
|
||
if (shouldInitEffects) { | ||
effectsRunner.start(); | ||
} | ||
|
||
for (const effectsClass of effects) { | ||
const effectsInstance = inject(effectsClass); | ||
effectSources.addEffects(effectsInstance); | ||
} | ||
|
||
if (shouldInitEffects) { | ||
const store = inject(Store); | ||
store.dispatch(effectsInit()); | ||
} | ||
}, | ||
}, | ||
], | ||
}; | ||
} |
Oops, something went wrong.