-
Notifications
You must be signed in to change notification settings - Fork 404
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(store): warn on unhandled actions (#1870)
Co-authored-by: Mark Whitfeld <[email protected]>
- Loading branch information
1 parent
7c829a7
commit 1bdb8c0
Showing
10 changed files
with
253 additions
and
4 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
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,50 @@ | ||
# Monitoring Unhandled Actions | ||
|
||
We can know if we have dispatched some actions which haven't been handled by any of the NGXS states. This is useful to monitor if we dispatch actions at the right time. For instance, dispatched actions might be coming from the WebSocket, but the action handler is located within the feature state that has not been registered yet. This will let us know that we should either register the state earlier or do anything else from the code perspective because actions are not being handled. | ||
|
||
This may be enabled by importing the `NgxsDevelopmentModule`. | ||
|
||
## Ignoring Certain Actions | ||
|
||
We can ignore specific actions that should not be logged if they have never been handled. E.g., if we're using the `@ngxs/router-plugin` and don't care about router actions like `RouterNavigation`, then we may add it to the `ignore` array: | ||
|
||
```ts | ||
import { NgxsDevelopmentModule } from '@ngxs/store'; | ||
import { RouterNavigation, RouterCancel } from '@ngxs/router-plugin'; | ||
|
||
@NgModule({ | ||
imports: [ | ||
NgxsDevelopmentModule.forRoot({ | ||
warnOnUnhandledActions: { | ||
ignore: [RouterNavigation, RouterCancel] | ||
} | ||
}) | ||
] | ||
}) | ||
export class AppModule {} | ||
``` | ||
|
||
It's best to import this module only in development mode. This may be achieved using environment imports. See [dynamic plugins](../recipes/dynamic-plugins.md). | ||
|
||
Ignored actions can be also expanded in lazy modules. The `@ngxs/store` exposes the `NgxsUnhandledActionsLogger` for these purposes: | ||
|
||
```ts | ||
import { Injector } from '@angular/core'; | ||
import { NgxsUnhandledActionsLogger } from '@ngxs/store'; | ||
|
||
declare const ngDevMode: boolean; | ||
|
||
@NgModule({ | ||
imports: [NgxsModule.forFeature([LazyState])] | ||
}) | ||
export class LazyModule { | ||
constructor(injector: Injector) { | ||
if (ngDevMode) { | ||
const unhandledActionsLogger = injector.get(NgxsUnhandledActionsLogger); | ||
unhandledActionsLogger.ignoreActions(LazyAction); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The `ngDevMode` is a specific variable provided by Angular in development mode and by Angular CLI (to Terser) in production mode. This allows tree-shaking `NgxsUnhandledActionsLogger` stuff since the `NgxsDevelopmentModule` is imported only in development mode. It's never functional in production mode. |
17 changes: 17 additions & 0 deletions
17
packages/store/src/dev-features/ngxs-development.module.ts
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,17 @@ | ||
import { ModuleWithProviders, NgModule } from '@angular/core'; | ||
|
||
import { NgxsDevelopmentOptions, NGXS_DEVELOPMENT_OPTIONS } from './symbols'; | ||
import { NgxsUnhandledActionsLogger } from './ngxs-unhandled-actions-logger'; | ||
|
||
@NgModule() | ||
export class NgxsDevelopmentModule { | ||
static forRoot(options: NgxsDevelopmentOptions): ModuleWithProviders<NgxsDevelopmentModule> { | ||
return { | ||
ngModule: NgxsDevelopmentModule, | ||
providers: [ | ||
NgxsUnhandledActionsLogger, | ||
{ provide: NGXS_DEVELOPMENT_OPTIONS, useValue: options } | ||
] | ||
}; | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
packages/store/src/dev-features/ngxs-unhandled-actions-logger.ts
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,48 @@ | ||
import { Inject, Injectable } from '@angular/core'; | ||
|
||
import { ActionType } from '../actions/symbols'; | ||
import { getActionTypeFromInstance } from '../utils/utils'; | ||
import { InitState, UpdateState } from '../actions/actions'; | ||
import { NgxsDevelopmentOptions, NGXS_DEVELOPMENT_OPTIONS } from './symbols'; | ||
|
||
@Injectable() | ||
export class NgxsUnhandledActionsLogger { | ||
/** | ||
* These actions should be ignored by default; the user can increase this | ||
* list in the future via the `ignoreActions` method. | ||
*/ | ||
private _ignoredActions = new Set<string>([InitState.type, UpdateState.type]); | ||
|
||
constructor(@Inject(NGXS_DEVELOPMENT_OPTIONS) options: NgxsDevelopmentOptions) { | ||
this.ignoreActions(...options.warnOnUnhandledActions.ignore); | ||
} | ||
|
||
/** | ||
* Adds actions to the internal list of actions that should be ignored. | ||
*/ | ||
ignoreActions(...actions: ActionType[]): void { | ||
for (const action of actions) { | ||
this._ignoredActions.add(action.type); | ||
} | ||
} | ||
|
||
/** @internal */ | ||
warn(action: any): void { | ||
const actionShouldBeIgnored = Array.from(this._ignoredActions).some( | ||
type => type === getActionTypeFromInstance(action) | ||
); | ||
|
||
if (actionShouldBeIgnored) { | ||
return; | ||
} | ||
|
||
action = | ||
action.constructor && action.constructor.name !== 'Object' | ||
? action.constructor.name | ||
: action.type; | ||
|
||
console.warn( | ||
`The ${action} action has been dispatched but hasn't been handled. This may happen if the state with an action handler for this action is not registered.` | ||
); | ||
} | ||
} |
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,19 @@ | ||
import { InjectionToken } from '@angular/core'; | ||
|
||
import { ActionType } from '../actions/symbols'; | ||
|
||
export interface NgxsDevelopmentOptions { | ||
warnOnUnhandledActions: { | ||
ignore: ActionType[]; | ||
}; | ||
} | ||
|
||
export const NGXS_DEVELOPMENT_OPTIONS = new InjectionToken<NgxsDevelopmentOptions>( | ||
'NGXS_DEVELOPMENT_OPTIONS', | ||
{ | ||
providedIn: 'root', | ||
factory: () => ({ | ||
warnOnUnhandledActions: { ignore: [] } | ||
}) | ||
} | ||
); |
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
Empty file.
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,92 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
|
||
import { Store, NgxsModule, NgxsDevelopmentModule, NgxsUnhandledActionsLogger } from '..'; | ||
|
||
describe('Unhandled actions warnings', () => { | ||
class FireAndForget { | ||
static readonly type = 'Fire & forget'; | ||
} | ||
|
||
const plainObjectAction = { type: 'Fire & Forget' }; | ||
|
||
const testSetup = (warnOnUnhandledActions = true) => { | ||
TestBed.configureTestingModule({ | ||
imports: [ | ||
NgxsModule.forRoot([]), | ||
warnOnUnhandledActions | ||
? NgxsDevelopmentModule.forRoot({ | ||
warnOnUnhandledActions: { ignore: [] } | ||
}) | ||
: [] | ||
] | ||
}); | ||
|
||
const store = TestBed.inject(Store); | ||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); | ||
const unhandledActionsLogger = TestBed.inject(NgxsUnhandledActionsLogger, null); | ||
return { store, warnSpy, unhandledActionsLogger }; | ||
}; | ||
|
||
it('should not warn on unhandled actions if the module is not provided', () => { | ||
// Arrange | ||
const { store, warnSpy } = testSetup(/* warnOnUnhandledActions */ false); | ||
// Act | ||
store.dispatch(new FireAndForget()); | ||
// Assert | ||
try { | ||
expect(warnSpy).not.toHaveBeenCalled(); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
|
||
describe('warn on unhandled actions', () => { | ||
it('should warn when the action is a class', () => { | ||
// Arrange | ||
const { store, warnSpy } = testSetup(); | ||
const action = new FireAndForget(); | ||
// Act | ||
store.dispatch(action); | ||
// Assert | ||
try { | ||
expect(warnSpy).toHaveBeenCalledTimes(1); | ||
expect(warnSpy).toHaveBeenCalledWith( | ||
`The FireAndForget action has been dispatched but hasn't been handled. This may happen if the state with an action handler for this action is not registered.` | ||
); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
|
||
it('should warn when the action is an object', () => { | ||
// Arrange | ||
const { store, warnSpy } = testSetup(); | ||
// Act | ||
store.dispatch(plainObjectAction); | ||
// Assert | ||
try { | ||
expect(warnSpy).toHaveBeenCalledTimes(1); | ||
expect(warnSpy).toHaveBeenCalledWith( | ||
`The Fire & Forget action has been dispatched but hasn't been handled. This may happen if the state with an action handler for this action is not registered.` | ||
); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
|
||
it('should be possible to add custom actions which should be ignored', () => { | ||
// Arrange | ||
const { store, warnSpy, unhandledActionsLogger } = testSetup(); | ||
unhandledActionsLogger!.ignoreActions(FireAndForget, plainObjectAction); | ||
// Act | ||
store.dispatch(new FireAndForget()); | ||
store.dispatch(plainObjectAction); | ||
// Assert | ||
try { | ||
expect(warnSpy).not.toHaveBeenCalled(); | ||
} finally { | ||
warnSpy.mockRestore(); | ||
} | ||
}); | ||
}); | ||
}); |