Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions modules/effects/spec/effects_feature_module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { firstValueFrom } from 'rxjs';
import { map, take, withLatestFrom } from 'rxjs/operators';
import { Actions, EffectsModule, ofType, createEffect } from '../';
import { FEATURE_EFFECTS_INIT } from '../src/effects_actions';
import { EffectsFeatureModule } from '../src/effects_feature_module';
import { EffectsRootModule } from '../src/effects_root_module';
import { _FEATURE_EFFECTS_INSTANCE_GROUPS } from '../src/tokens';
Expand All @@ -24,6 +25,7 @@ describe('Effects Feature Module', () => {
const effectSourceGroups = [[sourceA], [sourceB], [sourceC]];

let mockEffectSources: { addEffects: MockInstance };
let mockStore: { dispatch: MockInstance };

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -34,6 +36,12 @@ describe('Effects Feature Module', () => {
addEffects: vi.fn(),
},
},
{
provide: Store,
useValue: {
dispatch: vi.fn(),
},
},
{
provide: _FEATURE_EFFECTS_INSTANCE_GROUPS,
useValue: effectSourceGroups,
Expand All @@ -45,6 +53,9 @@ describe('Effects Feature Module', () => {
mockEffectSources = TestBed.inject<unknown>(EffectsRootModule) as {
addEffects: MockInstance;
};
mockStore = TestBed.inject<unknown>(Store) as {
dispatch: MockInstance;
};
});

it('should add all effects when instantiated', () => {
Expand All @@ -54,6 +65,14 @@ describe('Effects Feature Module', () => {
expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceB);
expect(mockEffectSources.addEffects).toHaveBeenCalledWith(sourceC);
});

it('should dispatch the feature effects init action', () => {
TestBed.inject(EffectsFeatureModule);

expect(mockStore.dispatch).toHaveBeenCalledWith({
type: FEATURE_EFFECTS_INIT,
});
});
});

describe('when registered in a different NgModule from the feature state', () => {
Expand Down
36 changes: 35 additions & 1 deletion modules/effects/spec/effects_root_module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { TestBed } from '@angular/core/testing';
import { INIT, Store, StoreModule } from '@ngrx/store';

import { EffectsModule } from '../src/effects_module';
import { ROOT_EFFECTS_INIT } from '../src/effects_actions';
import {
ROOT_EFFECTS_INIT,
FEATURE_EFFECTS_INIT,
} from '../src/effects_actions';

describe('Effects Root Module', () => {
const foo = 'foo';
Expand Down Expand Up @@ -47,4 +50,35 @@ describe('Effects Root Module', () => {
type: ROOT_EFFECTS_INIT,
});
});

it('dispatches the feature effects init action when EffectsModule.forFeature() is used', () => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({ reducer }, { initialState: { reducer: foo } }),
EffectsModule.forRoot([]),
EffectsModule.forFeature([]),
],
});

const store = TestBed.inject(Store);

expect(reducer).toHaveBeenCalledWith(foo, {
type: FEATURE_EFFECTS_INIT,
});
});

it(`doesn't dispatch the feature effects init action when EffectsModule.forFeature() isn't used`, () => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({ reducer }, { initialState: { reducer: foo } }),
EffectsModule.forRoot([]),
],
});

const store = TestBed.inject(Store);

expect(reducer).not.toHaveBeenCalledWith(foo, {
type: FEATURE_EFFECTS_INIT,
});
});
});
9 changes: 9 additions & 0 deletions modules/effects/spec/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
EffectsModule,
OnInitEffects,
ROOT_EFFECTS_INIT,
FEATURE_EFFECTS_INIT,
OnIdentifyEffects,
EffectSources,
Actions,
Expand Down Expand Up @@ -193,6 +194,8 @@ describe('NgRx Effects Integration spec', () => {
'[RootEffectWithInitAction2]: INIT',

ROOT_EFFECTS_INIT,

FEATURE_EFFECTS_INIT,
]);
});

Expand Down Expand Up @@ -292,12 +295,16 @@ describe('NgRx Effects Integration spec', () => {
// next feat effects
'[FeatEffectWithInitAction]: INIT',

FEATURE_EFFECTS_INIT,

// lastly added features (3 effects but 2 unique keys)
'[FeatEffectWithIdentifierAndInitAction]: INIT',
'[FeatEffectWithIdentifierAndInitAction]: INIT',

// from lazy loaded module
'[FeatEffectFromLazyLoadedModuleWithInitAction]: INIT',

FEATURE_EFFECTS_INIT,
];

// reducers should receive all actions
Expand Down Expand Up @@ -358,6 +365,8 @@ describe('NgRx Effects Integration spec', () => {

// User provided effects loaded by feature module
'[UserProvidedEffect2]: INIT',

FEATURE_EFFECTS_INIT,
];
expect(dispatchedActionsLog).toEqual(expectedLog);
});
Expand Down
3 changes: 3 additions & 0 deletions modules/effects/src/effects_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ import { createAction } from '@ngrx/store';

export const ROOT_EFFECTS_INIT = '@ngrx/effects/init';
export const rootEffectsInit = createAction(ROOT_EFFECTS_INIT);

export const FEATURE_EFFECTS_INIT = '@ngrx/effects/feature/init';
export const featureEffectsInit = createAction(FEATURE_EFFECTS_INIT);
6 changes: 5 additions & 1 deletion modules/effects/src/effects_feature_module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NgModule, Inject, Optional } from '@angular/core';
import { StoreRootModule, StoreFeatureModule } from '@ngrx/store';
import { Store, StoreRootModule, StoreFeatureModule } from '@ngrx/store';
import { FEATURE_EFFECTS_INIT } from './effects_actions';
import { EffectsRootModule } from './effects_root_module';
import { _FEATURE_EFFECTS_INSTANCE_GROUPS } from './tokens';

@NgModule({})
export class EffectsFeatureModule {
constructor(
effectsRootModule: EffectsRootModule,
store: Store,
@Inject(_FEATURE_EFFECTS_INSTANCE_GROUPS)
effectsInstanceGroups: unknown[][],
@Optional() storeRootModule: StoreRootModule,
Expand All @@ -16,5 +18,7 @@ export class EffectsFeatureModule {
for (const effectsInstance of effectsInstances) {
effectsRootModule.addEffects(effectsInstance);
}

store.dispatch({ type: FEATURE_EFFECTS_INIT });
}
}
7 changes: 6 additions & 1 deletion modules/effects/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export {
export { Actions, ofType } from './actions';
export { EffectsModule } from './effects_module';
export { EffectSources } from './effect_sources';
export { ROOT_EFFECTS_INIT, rootEffectsInit } from './effects_actions';
export {
ROOT_EFFECTS_INIT,
rootEffectsInit,
FEATURE_EFFECTS_INIT,
featureEffectsInit,
} from './effects_actions';
export { EffectsRunner } from './effects_runner';
export { EffectNotification } from './effect_notification';
export { EffectsFeatureModule } from './effects_feature_module';
Expand Down
20 changes: 20 additions & 0 deletions projects/www/src/app/pages/guide/effects/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ init$ = createEffect(() => {

</ngrx-code-example>

### FEATURE_EFFECTS_INIT

After all the root effects have been added, the feature effects are added, and after all of them are added too, the feature effect dispatches a `FEATURE_EFFECTS_INIT` action.
Like with `ROOT_EFFECTS_INIT`, you can see this action as a lifecycle hook, which you can use in order to execute some code after all your root and feature effects have been added.

<ngrx-code-example header="late-init.effects.ts">

```ts
lateInit$ = createEffect(() => {
return this.actions$.pipe(
ofType(FEATURE_EFFECTS_INIT),
map(action => ...)
);
});
```

</ngrx-code-example>

## Effect Metadata

### Non-dispatching Effects
Expand Down Expand Up @@ -181,6 +199,8 @@ class UserEffects implements OnInitEffects {
}
```

If there are too many effects in your app, you may instead consider relying on existing built-in actions `ROOT_EFFECTS_INIT` or `FEATURE_EFFECTS_INIT`.

</ngrx-code-example>

### OnRunEffects
Expand Down
Loading