diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1de6b7e..3ec8af6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ $ npm install @ngxs/store@dev ### To become next patch version -- ... +- Fix(store): Add root store initializer guard [#2278](https://github.com/ngxs/store/pull/2278) ### 19.0.0 2024-12-3 diff --git a/packages/store/src/standalone-features/initializers.ts b/packages/store/src/standalone-features/initializers.ts index eef13fd26..fee4b6cf7 100644 --- a/packages/store/src/standalone-features/initializers.ts +++ b/packages/store/src/standalone-features/initializers.ts @@ -7,6 +7,7 @@ import { InitState, UpdateState } from '../plugin_api'; import { FEATURE_STATE_TOKEN, ROOT_STATE_TOKEN } from '../symbols'; import { StateFactory } from '../internal/state-factory'; import { StatesAndDefaults } from '../internal/internals'; +import { assertRootStoreNotInitialized } from './root-guard'; import { SelectFactory } from '../decorators/select/select-factory'; import { InternalStateOperations } from '../internal/state-operations'; import { LifecycleStateManager } from '../internal/lifecycle-state-manager'; @@ -18,6 +19,10 @@ import { installOnUnhandhedErrorHandler } from '../internal/unhandled-rxjs-error * same initialization functionality. */ export function rootStoreInitializer(): void { + if (typeof ngDevMode !== 'undefined' && ngDevMode) { + assertRootStoreNotInitialized(); + } + // Override the RxJS `config.onUnhandledError` within the root store initializer, // but only after other code has already executed. // If users have a custom `config.onUnhandledError`, we might overwrite it too diff --git a/packages/store/src/standalone-features/root-guard.ts b/packages/store/src/standalone-features/root-guard.ts new file mode 100644 index 000000000..89ab9bc00 --- /dev/null +++ b/packages/store/src/standalone-features/root-guard.ts @@ -0,0 +1,14 @@ +import { inject, InjectionToken } from '@angular/core'; + +export const ROOT_STORE_GUARD = /* @__PURE__ */ new InjectionToken('ROOT_STORE_GUARD', { + providedIn: 'root', + factory: () => ({ initialized: false }) +}); + +export function assertRootStoreNotInitialized(): void { + const rootStoreGuard = inject(ROOT_STORE_GUARD); + if (rootStoreGuard.initialized) { + throw new Error('provideStore() should only be called once.'); + } + rootStoreGuard.initialized = true; +} diff --git a/packages/store/tests/issues/issue-2277-provide-store-twice.spec.ts b/packages/store/tests/issues/issue-2277-provide-store-twice.spec.ts new file mode 100644 index 000000000..877a220b7 --- /dev/null +++ b/packages/store/tests/issues/issue-2277-provide-store-twice.spec.ts @@ -0,0 +1,43 @@ +import { ApplicationConfig, Component } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter, Router } from '@angular/router'; +import { provideStore } from '@ngxs/store'; +import { freshPlatform, skipConsoleLogging } from '@ngxs/store/internals/testing'; + +describe('provideStore() being called twice', () => { + @Component({ selector: 'app-root', template: '' }) + class TestComponent {} + + @Component({ template: '' }) + class RouteComponent {} + + const appConfig: ApplicationConfig = { + providers: [ + provideStore(), + provideRouter([ + { path: 'route', loadComponent: () => RouteComponent, providers: [provideStore()] } + ]) + ] + }; + + it( + 'should throw an error when provideStore() is called twice', + freshPlatform(async () => { + // Arrange + expect.hasAssertions(); + + // Act + const { injector } = await skipConsoleLogging(() => + bootstrapApplication(TestComponent, appConfig) + ); + const router = injector.get(Router); + + try { + await router.navigateByUrl('/route'); + } catch (error) { + // Assert + expect(error.message).toEqual('provideStore() should only be called once.'); + } + }) + ); +});