NgRx State Management: Enterprise-Ready Angular Architecture
Master NgRx for scalable Angular applications. Learn actions, reducers, effects, and selectors to build maintainable state management solutions.
- Angular
- NgRx
- State Management
- TypeScript
- RxJS
NgRx provides a reactive state management solution for Angular applications using RxJS. After implementing it in production applications like ENGINE and Ship-Watch, I've seen how it scales beautifully for complex enterprise applications. While it has more boilerplate than Zustand, NgRx excels when you need predictable state updates, time-travel debugging, and strict architectural patterns.
Why NgRx?
NgRx offers:
- Predictable state updates: Single source of truth with immutable updates
- Time-travel debugging: Track every state change
- Testability: Pure functions make testing straightforward
- Scalability: Works well for large teams and complex applications
- DevTools integration: Powerful debugging capabilities
Core Concepts
NgRx follows the Redux pattern with Angular-specific implementations:
- Store: Single source of truth
- Actions: Events that trigger state changes
- Reducers: Pure functions that update state
- Effects: Side effects (API calls, logging)
- Selectors: Derived state and memoization
Basic Setup
Install NgRx:
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Actions
Define actions to describe events:
// actions/user.actions.ts
import { createAction, props } from '@ngrx/store';
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[User] Load Users Failure',
props<{ error: string }>()
);
export const addUser = createAction(
'[User] Add User',
props<{ user: User }>()
);
export const updateUser = createAction(
'[User] Update User',
props<{ id: string; changes: Partial<User> }>()
);
export const deleteUser = createAction(
'[User] Delete User',
props<{ id: string }>()
);
Reducers
Create reducers to handle state updates:
// reducers/user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as UserActions from '../actions/user.actions';
export interface UserState {
users: User[];
loading: boolean;
error: string | null;
}
export const initialState: UserState = {
users: [],
loading: false,
error: null,
};
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,
error,
loading: false,
})),
on(UserActions.addUser, (state, { user }) => ({
...state,
users: [...state.users, user],
})),
on(UserActions.updateUser, (state, { id, changes }) => ({
...state,
users: state.users.map(user =>
user.id === id ? { ...user, ...changes } : user
),
})),
on(UserActions.deleteUser, (state, { id }) => ({
...state,
users: state.users.filter(user => user.id !== id),
}))
);
Effects
Handle side effects like API calls:
// effects/user.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, catchError, switchMap } from 'rxjs/operators';
import * as UserActions from '../actions/user.actions';
import { UserService } from '../services/user.service';
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error =>
of(UserActions.loadUsersFailure({ error: error.message }))
)
)
)
)
);
addUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.addUser),
switchMap(({ user }) =>
this.userService.createUser(user).pipe(
map(newUser => UserActions.addUserSuccess({ user: newUser })),
catchError(error =>
of(UserActions.addUserFailure({ error: error.message }))
)
)
)
)
);
constructor(
private actions$: Actions,
private userService: UserService
) {}
}
Selectors
Create selectors for derived state:
// selectors/user.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { UserState } from '../reducers/user.reducer';
export const selectUserState = createFeatureSelector<UserState>('users');
export const selectAllUsers = createSelector(
selectUserState,
(state) => state.users
);
export const selectUsersLoading = createSelector(
selectUserState,
(state) => state.loading
);
export const selectUsersError = createSelector(
selectUserState,
(state) => state.error
);
export const selectUserById = (id: string) => createSelector(
selectAllUsers,
(users) => users.find(user => user.id === id)
);
export const selectActiveUsers = createSelector(
selectAllUsers,
(users) => users.filter(user => user.active)
);
App Module Configuration
Register store, effects, and devtools:
// app.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { userReducer } from './reducers/user.reducer';
import { UserEffects } from './effects/user.effects';
@NgModule({
imports: [
StoreModule.forRoot({ users: userReducer }),
EffectsModule.forRoot([UserEffects]),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,
}),
],
})
export class AppModule {}
Using in Components
Dispatch actions and select state:
// components/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as UserActions from '../actions/user.actions';
import * as UserSelectors from '../selectors/user.selectors';
import { User } from '../models/user.model';
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<div *ngIf="error$ | async as error">{{ error }}</div>
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
<button (click)="loadUsers()">Load Users</button>
`,
})
export class UserListComponent implements OnInit {
users$: Observable<User[]> = this.store.select(UserSelectors.selectAllUsers);
loading$: Observable<boolean> = this.store.select(UserSelectors.selectUsersLoading);
error$: Observable<string | null> = this.store.select(UserSelectors.selectUsersError);
constructor(private store: Store) {}
ngOnInit(): void {
this.loadUsers();
}
loadUsers(): void {
this.store.dispatch(UserActions.loadUsers());
}
addUser(user: User): void {
this.store.dispatch(UserActions.addUser({ user }));
}
}
Entity Adapter
For collections, use EntityAdapter:
// reducers/user.reducer.ts
import { createEntityAdapter, EntityState } from '@ngrx/entity';
export interface UserState extends EntityState<User> {
loading: boolean;
error: string | null;
}
export const userAdapter = createEntityAdapter<User>({
selectId: (user: User) => user.id,
sortComparer: (a: User, b: User) => a.name.localeCompare(b.name),
});
export const initialState: UserState = userAdapter.getInitialState({
loading: false,
error: null,
});
export const userReducer = createReducer(
initialState,
on(UserActions.loadUsersSuccess, (state, { users }) =>
userAdapter.setAll(users, { ...state, loading: false })
),
on(UserActions.addUser, (state, { user }) =>
userAdapter.addOne(user, state)
),
on(UserActions.updateUser, (state, { id, changes }) =>
userAdapter.updateOne({ id, changes }, state)
),
on(UserActions.deleteUser, (state, { id }) =>
userAdapter.removeOne(id, state)
)
);
// Selectors with EntityAdapter
export const {
selectAll: selectAllUsers,
selectEntities: selectUserEntities,
selectIds: selectUserIds,
selectTotal: selectUserTotal,
} = userAdapter.getSelectors(selectUserState);
Facade Pattern
Create facades to simplify component interactions:
// facades/user.facade.ts
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as UserActions from '../actions/user.actions';
import * as UserSelectors from '../selectors/user.selectors';
import { User } from '../models/user.model';
@Injectable({ providedIn: 'root' })
export class UserFacade {
users$: Observable<User[]> = this.store.select(UserSelectors.selectAllUsers);
loading$: Observable<boolean> = this.store.select(UserSelectors.selectUsersLoading);
error$: Observable<string | null> = this.store.select(UserSelectors.selectUsersError);
constructor(private store: Store) {}
loadUsers(): void {
this.store.dispatch(UserActions.loadUsers());
}
addUser(user: User): void {
this.store.dispatch(UserActions.addUser({ user }));
}
updateUser(id: string, changes: Partial<User>): void {
this.store.dispatch(UserActions.updateUser({ id, changes }));
}
deleteUser(id: string): void {
this.store.dispatch(UserActions.deleteUser({ id }));
}
}
Best Practices
- Use EntityAdapter: For collections, EntityAdapter reduces boilerplate
- Create facades: Simplify component interactions
- Use selectors: Memoize derived state
- Keep reducers pure: No side effects in reducers
- Handle errors: Always handle error cases in effects
- Use DevTools: Leverage NgRx DevTools for debugging
- Split by feature: Organize store by feature modules
Real-World Impact
In production applications, NgRx has provided:
- Predictable state management for complex workflows
- Excellent debugging capabilities with DevTools
- Better testability with pure functions
- Scalable architecture for large teams
- Time-travel debugging for issue resolution
NgRx is powerful for enterprise Angular applications where predictability, testability, and scalability are crucial. While it has more boilerplate than simpler solutions, the benefits become clear as applications grow in complexity.
Keep reading
Angular Performance Optimization: A Complete Guide
Learn advanced techniques to optimize Angular applications, from change detection to lazy loading and bundle optimization.
RxJS Essentials: Most Used Operators and Patterns
Master the most commonly used RxJS operators and patterns for handling async operations, data transformation, and reactive programming in Angular.
State Management with Zustand: A Modern Approach
Learn how to use Zustand for lightweight, performant state management in React applications without the boilerplate of Redux.