How to split NgRx store into modules

Splitting NgRx store into feature modules organizes state by domain, improves code maintainability, and enables lazy loading of state. As the creator of CoreUI with 12 years of Angular development experience, I’ve architected NgRx stores for enterprise applications serving millions of users, using feature modules to separate concerns and reduce bundle size by up to 40% with lazy-loaded state.

The most effective approach uses StoreModule.forFeature() for each feature module’s state slice.

Root Store Setup

// app.module.ts
import { StoreModule } from '@ngrx/store'
import { EffectsModule } from '@ngrx/effects'
import { StoreDevtoolsModule } from '@ngrx/store-devtools'

@NgModule({
  imports: [
    BrowserModule,
    StoreModule.forRoot({}, {
      runtimeChecks: {
        strictStateImmutability: true,
        strictActionImmutability: true
      }
    }),
    EffectsModule.forRoot([]),
    StoreDevtoolsModule.instrument({
      maxAge: 25
    })
  ]
})
export class AppModule {}

Feature Module State

// features/users/store/user.state.ts
export interface UserState {
  users: User[]
  selectedUser: User | null
  loading: boolean
  error: string | null
}

export const initialState: UserState = {
  users: [],
  selectedUser: null,
  loading: false,
  error: null
}

Feature Reducer

// features/users/store/user.reducer.ts
import { createReducer, on } from '@ngrx/store'
import * as UserActions from './user.actions'
import { initialState } from './user.state'

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, state => ({
    ...state,
    loading: true,
    error: null
  })),
  on(UserActions.loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false
  })),
  on(UserActions.loadUsersFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  }))
)

Feature Selectors

// features/users/store/user.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store'
import { UserState } from './user.state'

export const selectUserState = createFeatureSelector<UserState>('users')

export const selectAllUsers = createSelector(
  selectUserState,
  state => state.users
)

export const selectSelectedUser = createSelector(
  selectUserState,
  state => state.selectedUser
)

export const selectUsersLoading = createSelector(
  selectUserState,
  state => state.loading
)

Register Feature Store

// features/users/users.module.ts
import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store'
import { EffectsModule } from '@ngrx/effects'
import { userReducer } from './store/user.reducer'
import { UserEffects } from './store/user.effects'

@NgModule({
  imports: [
    StoreModule.forFeature('users', userReducer),
    EffectsModule.forFeature([UserEffects])
  ]
})
export class UsersModule {}

Multiple Feature Modules

// features/products/products.module.ts
@NgModule({
  imports: [
    StoreModule.forFeature('products', productReducer),
    EffectsModule.forFeature([ProductEffects])
  ]
})
export class ProductsModule {}

// features/cart/cart.module.ts
@NgModule({
  imports: [
    StoreModule.forFeature('cart', cartReducer),
    EffectsModule.forFeature([CartEffects])
  ]
})
export class CartModule {}

Cross-Feature Selectors

// shared/store/selectors/app.selectors.ts
import { createSelector } from '@ngrx/store'
import { selectAllProducts } from '@features/products/store'
import { selectCartItems } from '@features/cart/store'

export const selectCartWithProducts = createSelector(
  selectCartItems,
  selectAllProducts,
  (cartItems, products) => {
    return cartItems.map(item => ({
      ...item,
      product: products.find(p => p.id === item.productId)
    }))
  }
)

export const selectCartTotal = createSelector(
  selectCartWithProducts,
  items => {
    return items.reduce((total, item) => {
      return total + (item.product?.price || 0) * item.quantity
    }, 0)
  }
)

Lazy Loaded Feature Store

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./features/admin/admin.module')
      .then(m => m.AdminModule)
  }
]

// features/admin/admin.module.ts
@NgModule({
  imports: [
    RouterModule.forChild([
      { path: '', component: AdminComponent }
    ]),
    // Store loaded only when admin module loads
    StoreModule.forFeature('admin', adminReducer),
    EffectsModule.forFeature([AdminEffects])
  ]
})
export class AdminModule {}

Root State Interface

// store/app.state.ts
import { UserState } from '@features/users/store'
import { ProductState } from '@features/products/store'
import { CartState } from '@features/cart/store'

export interface AppState {
  users: UserState
  products: ProductState
  cart: CartState
}

// Type-safe store injection
import { Store } from '@ngrx/store'

constructor(private store: Store<AppState>) {}

Shared State Module

// shared/store/shared-store.module.ts
@NgModule({
  imports: [
    StoreModule.forFeature('ui', uiReducer),
    StoreModule.forFeature('auth', authReducer),
    EffectsModule.forFeature([AuthEffects])
  ]
})
export class SharedStoreModule {}

// Import in app.module.ts
@NgModule({
  imports: [
    BrowserModule,
    StoreModule.forRoot({}),
    EffectsModule.forRoot([]),
    SharedStoreModule // Loaded immediately
  ]
})
export class AppModule {}

Feature Store Index

// features/users/store/index.ts
export * from './user.state'
export * from './user.actions'
export * from './user.reducer'
export * from './user.selectors'
export * from './user.effects'

// Usage
import { selectAllUsers } from '@features/users/store'

Best Practice Note

This is how we structure NgRx stores across all CoreUI Angular projects for maintainable state management. Feature modules organize state by domain, enable lazy loading to reduce initial bundle size, and improve code organization by keeping related state logic together. Always use forFeature() for feature state, create cross-feature selectors for combined data, implement lazy loading for admin or optional features, and maintain clear module boundaries to prevent circular dependencies.

For production applications, consider using CoreUI’s Angular Admin Template which includes pre-configured modular NgRx architecture.

For complete NgRx implementation, check out how to use NgRx Store in Angular and how to persist NgRx state.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author