skip to content

In NgRx 22, how does provideStore() differ from StoreModule.forRoot(), and where must a standalone app register the root store?

level: middleimportance: should knowfreq 42%

answer

  1. same store, different container
  2. EnvironmentProviders vs ModuleWithProviders
  3. application providers, once
  4. root provided more than once
  5. same RootStoreConfig argument

basics

~20 s

provideStore() and StoreModule.forRoot() configure the same root store with the same arguments. provideStore() returns EnvironmentProviders for a standalone app's application config; forRoot() goes in an NgModule's imports. Register the root once; a second registration throws.

solid answer

~30 s

Both take a reducer map and a `RootStoreConfig` (`initialState`, `metaReducers`, `reducerFactory`, `runtimeChecks`) and build the same root store. `provideStore()` is the standalone spelling: it returns `EnvironmentProviders`, so it goes in `bootstrapApplication` or `app.config.ts`, never in a component's `providers`. `StoreModule.forRoot()` returns `ModuleWithProviders` for an NgModule's `imports`, and it still ships in NgRx 22. The root must be registered once: providing it again in a lazy route throws "The root Store has been provided more than once". Lazy areas add feature state instead. Migrating is mechanical, because the arguments move over unchanged.

go deeper

for a junior

Recall that a standalone app calls provideStore() in its application config, and that older NgModule apps import StoreModule.forRoot() instead.

for a middle

Explain that both build the same root store from the same arguments, what EnvironmentProviders means for placement, and why a second root registration throws.

for a senior

Show you can migrate an NgModule store setup to standalone providers without behaviour change, and spot a duplicate root or a component-scoped provideStore in review.

for a principal

Treat the root store's registration as an app-shell concern: one owner, features extend it, and migrations happen as mechanical swaps rather than redesigns.

## Two spellings of one root store NgRx ships two ways to register the **root store**, the application-wide `Store` with its reducers, actions stream and state: - `provideStore(reducers?, config?)`, the **standalone** API, added in NgRx 14.3 and the default in current NgRx; - `StoreModule.forRoot(reducers?, config?)`, the **NgModule** API, which still ships in NgRx 22. Both take the same arguments and build the same internal provider list. What differs is the container they return, where that container is allowed to go, and what starts the store. | | `provideStore()` | `StoreModule.forRoot()` | |---|---|---| | Returns | `EnvironmentProviders` | `ModuleWithProviders<StoreRootModule>` | | Goes in | `bootstrapApplication` providers or `app.config.ts` | an NgModule's `imports` array | | Arguments | reducer map or token, plus `RootStoreConfig` | the same | | Store start-up | an environment initializer | the `StoreRootModule` constructor | | Status in 22 | current default | supported, older spelling | ## Where the root store must be registered In a standalone application the root store belongs in the **application's providers**: ```ts import { ApplicationConfig } from '@angular/core'; import { provideStore } from '@ngrx/store'; import { catalogueReducer } from './catalogue/catalogue.reducer'; export const appConfig: ApplicationConfig = { providers: [ provideStore({ catalogue: catalogueReducer }), ], }; ``` Three placement rules follow from the return type and from a guard in the source: 1. **Not in a component's `providers`.** `provideStore` returns `EnvironmentProviders`, which Angular accepts only in environment injectors (the application's providers and route `providers`), never in a component's `providers` array. The JSDoc says it plainly: these providers cannot be used at the component level. 2. **Only once.** The root providers include a guard that looks for a `Store` in a parent injector. If a lazy route's `providers` calls `provideStore()` again, start-up throws a `TypeError`: "The root Store has been provided more than once. Feature modules should provide feature states instead." 3. **Features go elsewhere.** A lazily loaded area adds its slice with the feature provider, not a second root. How feature slices register is a separate subject. `provideStore()` with no arguments is common: the root starts empty and every slice arrives as a feature. ## The config argument Both APIs take the same `RootStoreConfig` object as the second argument: - `initialState`, to seed the root state; - `metaReducers`, functions that wrap every reducer; - `reducerFactory`, to replace the default way reducers combine; - `runtimeChecks`, the development-time checks on state and actions. Their behaviour is covered with reducers. For setup, the point is that **moving from `forRoot` to `provideStore` needs no config changes**: the object moves over unchanged. ## Migrating an NgModule app A typical migration during a move to standalone bootstrapping: ```ts // Before, in AppModule imports: [ StoreModule.forRoot({ catalogue: catalogueReducer }, { metaReducers }), StoreDevtoolsModule.instrument({ maxAge: 25 }), ] // After, in app.config.ts providers: [ provideStore({ catalogue: catalogueReducer }, { metaReducers }), provideStoreDevtools({ maxAge: 25 }), ] ``` The other NgRx packages follow the same pattern: `provideEffects` replaces `EffectsModule.forRoot`, and `provideStoreDevtools` replaces `StoreDevtoolsModule.instrument`. ## Mistakes worth naming - Believing `StoreModule` was **removed**. It still ships in 22; `provideStore` is the preferred spelling, not a replacement that deleted the old one. - Believing `provideStore` **cannot take reducers or config**. It takes both, exactly like `forRoot`. - Calling `provideStore()` again in a lazy route to "add" reducers. That hits the root guard; the feature provider is the right tool. - Registering the store in a component's providers to scope it. A per-component store is what a local store is for, not the global `Store`. ## Checking a setup in review When reading an unfamiliar app's store setup, three quick checks cover most problems: 1. Search for `provideStore(` and `StoreModule.forRoot(`: there should be **exactly one** root registration between them. 2. Check that it sits in the **application** providers or the root NgModule, not in a component or a lazy route. 3. Check that `provideStoreDevtools` is also registered once, at the application level, next to the root store rather than inside a feature. The interview answer is short: same store, same arguments, different container. `provideStore` goes in the application providers once, and everything else in the app extends that single root.

  • Why can't provideStore() go in a component's providers array?
    It returns `EnvironmentProviders`, which Angular only accepts in environment injectors such as the application config or a route's `providers`. The global store is an application-level service; a component-scoped store is a different tool, a local store, not a second copy of `Store`.
  • An app calls provideStore() in app.config.ts and again in a lazy route's providers; what happens?
    The root providers include a guard that checks for a `Store` in a parent injector. The lazy route's injector finds the application's one, so start-up throws a `TypeError`: "The root Store has been provided more than once. Feature modules should provide feature states instead." The lazy area should register its slice with the feature provider.

saying these in an interview costs you the question

  • StoreModule was removed from NgRx when the standalone providers arrived.
  • provideStore() takes no arguments; reducers and runtime checks need StoreModule.
  • You can call provideStore() in each lazy route to add that route's reducers.
  • Put provideStore() in a component's providers to give it a private store.
  • provideStore() and forRoot() create different store implementations with different behaviour.