Angular State Management
State is any data that an application needs to remember and display — the current user, the items in a shopping cart, whether a sidebar is open, the results of a search. State management refers to the strategies and tools used to store, update, and share this data in a consistent and predictable way across the entire application.
In small applications, passing data through component inputs and services is sufficient. As an application grows, managing state becomes more complex. Multiple components may need the same data, data may need to be updated from many places, and keeping everything in sync becomes challenging. State management solutions address these challenges.
Types of State in Angular
Local Component State
Data that is only needed within a single component — such as whether a dropdown menu is open or the current value typed in a search field. This is managed directly in the component's TypeScript class with simple properties.
Shared State
Data needed by multiple unrelated components — such as the logged-in user's name (used by the header, profile page, and settings page). This is managed in a service using BehaviorSubject or a state management library.
Server State
Data fetched from an external API. This includes the response from HTTP requests, the loading state (is data being fetched?), and any error state.
State Management with Services (Simple Approach)
For many Angular applications, using a service with BehaviorSubject is sufficient for state management without any third-party library.
// store/app-state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
interface AppState {
user: { name: string; email: string } | null;
cartItemCount: number;
isLoading: boolean;
theme: 'light' | 'dark';
}
const initialState: AppState = {
user: null,
cartItemCount: 0,
isLoading: false,
theme: 'light'
};
@Injectable({ providedIn: 'root' })
export class AppStateService {
private state$ = new BehaviorSubject<AppState>(initialState);
// Selectors — expose specific slices of state as Observables
user$: Observable<AppState['user']> = this.state$.asObservable()
.pipe(map(state => state.user));
cartItemCount$: Observable<number> = this.state$.asObservable()
.pipe(map(state => state.cartItemCount));
isLoading$: Observable<boolean> = this.state$.asObservable()
.pipe(map(state => state.isLoading));
// Actions — methods that update the state
setUser(user: AppState['user']) {
this.setState({ user });
}
incrementCart() {
const current = this.state$.value;
this.setState({ cartItemCount: current.cartItemCount + 1 });
}
decrementCart() {
const current = this.state$.value;
const newCount = Math.max(0, current.cartItemCount - 1);
this.setState({ cartItemCount: newCount });
}
setLoading(isLoading: boolean) {
this.setState({ isLoading });
}
toggleTheme() {
const current = this.state$.value;
this.setState({ theme: current.theme === 'light' ? 'dark' : 'light' });
}
private setState(partial: Partial<AppState>) {
this.state$.next({ ...this.state$.value, ...partial });
}
}
// Using the state service in a component
import { Component, OnInit } from '@angular/core';
import { AppStateService } from '../store/app-state.service';
@Component({
selector: 'app-navbar',
template: `
<nav>
<span *ngIf="user$ | async as user">Welcome, {{ user.name }}</span>
<span>Cart: {{ cartCount$ | async }}</span>
<button (click)="logout()">Logout</button>
</nav>
`
})
export class NavbarComponent {
user$ = this.stateService.user$;
cartCount$ = this.stateService.cartItemCount$;
constructor(private stateService: AppStateService) {}
logout() {
this.stateService.setUser(null);
}
}
Introduction to NgRx
NgRx is the most widely used state management library for Angular. It is inspired by Redux and implements a unidirectional data flow pattern. NgRx is appropriate for large, complex applications where state changes need to be fully predictable and traceable.
Core NgRx Concepts
Store
The store is a single, centralized object that holds the entire application state as a plain JavaScript object. There is only one store per application.
Actions
Actions are plain objects that describe what happened in the application. They have a type property (a string describing the event) and optionally a payload with data.
Reducers
Reducers are pure functions that take the current state and an action and return the new state. They never modify the existing state — they always return a new state object. This predictability is what makes state changes traceable.
Selectors
Selectors are functions that extract specific pieces of state from the store. Components subscribe to selectors to receive the data they need.
Effects
Effects handle side effects — such as HTTP requests. They listen for specific actions, perform asynchronous operations, and dispatch new actions with the results.
NgRx Data Flow
Component dispatches Action
↓
Reducer receives Action + current State → returns New State
↓
Store updates with New State
↓
Selector extracts relevant data from New State
↓
Component receives updated data via Observable
Installing NgRx
ng add @ngrx/store @ngrx/effects @ngrx/store-devtools
NgRx Example — Shopping Cart
// store/cart/cart.actions.ts
import { createAction, props } from '@ngrx/store';
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
export const addItem = createAction(
'[Cart] Add Item',
props<{ item: CartItem }>()
);
export const removeItem = createAction(
'[Cart] Remove Item',
props<{ id: number }>()
);
export const clearCart = createAction('[Cart] Clear Cart');
// store/cart/cart.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { addItem, removeItem, clearCart, CartItem } from './cart.actions';
export interface CartState {
items: CartItem[];
}
const initialState: CartState = {
items: []
};
export const cartReducer = createReducer(
initialState,
on(addItem, (state, { item }) => {
const existingItem = state.items.find(i => i.id === item.id);
if (existingItem) {
return {
...state,
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
)
};
}
return { ...state, items: [...state.items, { ...item, quantity: 1 }] };
}),
on(removeItem, (state, { id }) => ({
...state,
items: state.items.filter(i => i.id !== id)
})),
on(clearCart, state => ({ ...state, items: [] }))
);
// store/cart/cart.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { CartState } from './cart.reducer';
export const selectCartState = createFeatureSelector<CartState>('cart');
export const selectCartItems = createSelector(
selectCartState,
(state) => state.items
);
export const selectCartCount = createSelector(
selectCartItems,
(items) => items.reduce((count, item) => count + item.quantity, 0)
);
export const selectCartTotal = createSelector(
selectCartItems,
(items) => items.reduce((total, item) => total + (item.price * item.quantity), 0)
);
// Using NgRx in a component
import { Store } from '@ngrx/store';
import { addItem, clearCart } from '../store/cart/cart.actions';
import { selectCartItems, selectCartCount, selectCartTotal } from '../store/cart/cart.selectors';
export class CartComponent {
cartItems$ = this.store.select(selectCartItems);
cartCount$ = this.store.select(selectCartCount);
cartTotal$ = this.store.select(selectCartTotal);
constructor(private store: Store) {}
addProduct() {
this.store.dispatch(addItem({
item: { id: 1, name: 'Laptop Stand', price: 39.99, quantity: 1 }
}));
}
onClear() {
this.store.dispatch(clearCart());
}
}
When to Use What
Use Component Properties (Local State)
- UI state used only within a single component (toggle, input value).
Use a Service with BehaviorSubject
- Small to medium applications.
- State shared between a handful of related components.
- Team prefers minimal dependencies.
Use NgRx
- Large applications with complex, deeply nested state.
- State changes must be fully traceable and debuggable.
- Multiple developers working on the same codebase need strict patterns.
- Time-travel debugging is needed.
Summary
State management organizes how application data is stored, updated, and shared. Angular supports multiple approaches depending on complexity. Local component state uses properties for data needed by a single component. Services with BehaviorSubject provide a lightweight, no-library approach for shared state across multiple components. NgRx provides a structured, Redux-inspired pattern for large applications with Actions (what happened), Reducers (how state changes), the Store (single source of truth), Selectors (read data), and Effects (handle side effects like API calls). Choosing the right approach depends on the size and complexity of the application.
