Skip to content

Commit 35fea5c

Browse files
authored
fix(store): add root store initializer guard (#2278)
In this commit, we are adding the root store initializer guard, which ensures that `provideStore()` is not accidentally called multiple times (e.g., in feature-level providers such as route providers).
1 parent 9deedc7 commit 35fea5c

File tree

4 files changed

+63
-1
lines changed

4 files changed

+63
-1
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ $ npm install @ngxs/store@dev
66

77
### To become next patch version
88

9-
- ...
9+
- Fix(store): Add root store initializer guard [#2278](https://github.com/ngxs/store/pull/2278)
1010

1111
### 19.0.0 2024-12-3
1212

packages/store/src/standalone-features/initializers.ts

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { InitState, UpdateState } from '../plugin_api';
77
import { FEATURE_STATE_TOKEN, ROOT_STATE_TOKEN } from '../symbols';
88
import { StateFactory } from '../internal/state-factory';
99
import { StatesAndDefaults } from '../internal/internals';
10+
import { assertRootStoreNotInitialized } from './root-guard';
1011
import { SelectFactory } from '../decorators/select/select-factory';
1112
import { InternalStateOperations } from '../internal/state-operations';
1213
import { LifecycleStateManager } from '../internal/lifecycle-state-manager';
@@ -18,6 +19,10 @@ import { installOnUnhandhedErrorHandler } from '../internal/unhandled-rxjs-error
1819
* same initialization functionality.
1920
*/
2021
export function rootStoreInitializer(): void {
22+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
23+
assertRootStoreNotInitialized();
24+
}
25+
2126
// Override the RxJS `config.onUnhandledError` within the root store initializer,
2227
// but only after other code has already executed.
2328
// If users have a custom `config.onUnhandledError`, we might overwrite it too
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { inject, InjectionToken } from '@angular/core';
2+
3+
export const ROOT_STORE_GUARD = /* @__PURE__ */ new InjectionToken('ROOT_STORE_GUARD', {
4+
providedIn: 'root',
5+
factory: () => ({ initialized: false })
6+
});
7+
8+
export function assertRootStoreNotInitialized(): void {
9+
const rootStoreGuard = inject(ROOT_STORE_GUARD);
10+
if (rootStoreGuard.initialized) {
11+
throw new Error('provideStore() should only be called once.');
12+
}
13+
rootStoreGuard.initialized = true;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ApplicationConfig, Component } from '@angular/core';
2+
import { bootstrapApplication } from '@angular/platform-browser';
3+
import { provideRouter, Router } from '@angular/router';
4+
import { provideStore } from '@ngxs/store';
5+
import { freshPlatform, skipConsoleLogging } from '@ngxs/store/internals/testing';
6+
7+
describe('provideStore() being called twice', () => {
8+
@Component({ selector: 'app-root', template: '' })
9+
class TestComponent {}
10+
11+
@Component({ template: '' })
12+
class RouteComponent {}
13+
14+
const appConfig: ApplicationConfig = {
15+
providers: [
16+
provideStore(),
17+
provideRouter([
18+
{ path: 'route', loadComponent: () => RouteComponent, providers: [provideStore()] }
19+
])
20+
]
21+
};
22+
23+
it(
24+
'should throw an error when provideStore() is called twice',
25+
freshPlatform(async () => {
26+
// Arrange
27+
expect.hasAssertions();
28+
29+
// Act
30+
const { injector } = await skipConsoleLogging(() =>
31+
bootstrapApplication(TestComponent, appConfig)
32+
);
33+
const router = injector.get(Router);
34+
35+
try {
36+
await router.navigateByUrl('/route');
37+
} catch (error) {
38+
// Assert
39+
expect(error.message).toEqual('provideStore() should only be called once.');
40+
}
41+
})
42+
);
43+
});

0 commit comments

Comments
 (0)