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.
Related Articles
For complete NgRx implementation, check out how to use NgRx Store in Angular and how to persist NgRx state.



