Skip to content

Commit

Permalink
fix(store): show error when state initialization order is invalid (#2066
Browse files Browse the repository at this point in the history
)

This commit adds a `console.error` when the `UpdateState` is dispatched before
the `InitState`. This typically indicates that the state initialization order is
invalid and must be updated to ensure that states are initialized in the correct
order. The incorrect order may prevent `ngxsOnInit` from being called on feature
states or lead to other unpredictable errors.
  • Loading branch information
arturovt authored Sep 27, 2023
1 parent 5942e46 commit b132a8a
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ $ npm install @ngxs/store@dev

### To become next patch version

- Fix: Show error when state initialization order is invalid [#2066](https://github.com/ngxs/store/pull/2066)
- Fix: Storage Plugin - Access local and session storages globals only in browser [#2034](https://github.com/ngxs/store/pull/2034)
- Fix: Storage Plugin - Require only `getItem` and `setItem` on engines [#2036](https://github.com/ngxs/store/pull/2036)
- Fix: Router Plugin - Expose `NGXS_ROUTER_PLUGIN_OPTIONS` privately [#2037](https://github.com/ngxs/store/pull/2037)
Expand Down
8 changes: 8 additions & 0 deletions packages/store/src/configs/messages.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export function getUndecoratedStateInIvyWarningMessage(name: string): string {
return `'${name}' class should be decorated with @Injectable() right after the @State() decorator`;
}

export function getInvalidInitializationOrderMessage() {
return (
'You have an invalid state initialization order. This typically occurs when `NgxsModule.forFeature`\n' +
'or `provideStates` is called before `NgxsModule.forRoot` or `provideStore`.\n' +
'One example is when `NgxsRouterPluginModule.forRoot` is called before `NgxsModule.forRoot`.'
);
}

export function throwSelectFactoryNotConnectedError(): never {
throw new Error('You have forgotten to import the NGXS module!');
}
Expand Down
27 changes: 26 additions & 1 deletion packages/store/src/internal/lifecycle-state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ import {
import { Store } from '../store';
import { getValue } from '../utils/utils';
import { InternalErrorReporter } from './error-handler';
import { InitState, UpdateState } from '../actions/actions';
import { StateContextFactory } from './state-context-factory';
import { InternalStateOperations } from './state-operations';
import { MappedStore, StatesAndDefaults } from './internals';
import { NgxsLifeCycle, NgxsSimpleChange, StateContext } from '../symbols';
import { getInvalidInitializationOrderMessage } from '../configs/messages.config';

const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode;

@Injectable({ providedIn: 'root' })
export class LifecycleStateManager implements OnDestroy {
private readonly _destroy$ = new ReplaySubject<void>(1);

private _initStateHasBeenDispatched?: boolean;

constructor(
private _store: Store,
private _internalErrorReporter: InternalErrorReporter,
Expand All @@ -35,7 +41,26 @@ export class LifecycleStateManager implements OnDestroy {
this._destroy$.next();
}

ngxsBootstrap<T>(action: T, results: StatesAndDefaults | undefined): void {
ngxsBootstrap(
action: InitState | UpdateState,
results: StatesAndDefaults | undefined
): void {
if (NG_DEV_MODE) {
if (action instanceof InitState) {
this._initStateHasBeenDispatched = true;
} else if (
// This is a dev mode-only check that ensures the correct order of
// state initialization. The `NgxsModule.forRoot` or `provideStore` should
// always come first, followed by `forFeature` and `provideStates`. If the
// `UpdateState` is dispatched before the `InitState` is dispatched, it indicates
// that modules or providers are in an invalid order.
action instanceof UpdateState &&
!this._initStateHasBeenDispatched
) {
console.error(getInvalidInitializationOrderMessage());
}
}

this._internalStateOperations
.getRootStateOperations()
.dispatch(action)
Expand Down
30 changes: 30 additions & 0 deletions packages/store/tests/issues/state-initialization-order.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { NgxsModule, State, Store } from '@ngxs/store';

describe('State stream order of updates', () => {
@Injectable()
@State({ name: 'counter', defaults: 0 })
class CounterState {}

it('should log an error into the console when the state initialization order is invalid', () => {
// Arrange
const errorSpy = jest.spyOn(console, 'error').mockImplementation();

TestBed.configureTestingModule({
imports: [NgxsModule.forFeature([CounterState]), NgxsModule.forRoot()]
});

// Act
TestBed.inject(Store);

try {
// Assert
expect(errorSpy).toHaveBeenCalledWith(
expect.stringMatching(/invalid state initialization order/)
);
} finally {
errorSpy.mockRestore();
}
});
});

0 comments on commit b132a8a

Please sign in to comment.