Welcome to our deep dive into state management with RxJS! In this installment, we’ll explore how to manage application state reactively using Angular’s built-in tools before considering libraries like NgRx.
Why RxJS for State Management?
RxJS provides:
- A unified way to handle async operations
- Powerful operators for state transformations
- Efficient change detection
- Scalable patterns for small to medium apps
- The foundation for more advanced solutions like NgRx
Core RxJS Concepts for State
1. Subjects – The State Containers
typescript
import { BehaviorSubject } from 'rxjs'; // Simple state store class CartStore { private _cartItems = new BehaviorSubject<CartItem[]>([]); // Read-only stream cartItems$ = this._cartItems.asObservable(); // Update method addItem(item: CartItem) { const current = this._cartItems.value; this._cartItems.next([...current, item]); } }
2. Combining State with Observables
typescript
// Combine multiple state sources userProfile$ = combineLatest([ this.userService.currentUser$, this.prefsService.userPreferences$ ]).pipe( map(([user, prefs]) => ({ ...user, ...prefs })) );
Reactive Service Pattern
A scalable approach for medium complexity apps:
typescript
@Injectable({ providedIn: 'root' }) export class ProductService { // Private state subject private productsSubject = new BehaviorSubject<Product[]>([]); // Public observable products$ = this.productsSubject.asObservable(); // Current snapshot get currentProducts() { return this.productsSubject.value; } constructor(private http: HttpClient) { this.loadProducts(); } loadProducts() { this.http.get<Product[]>('/api/products').subscribe( products => this.productsSubject.next(products) ); } addProduct(product: Product) { this.http.post('/api/products', product).subscribe(() => { this.productsSubject.next([ ...this.currentProducts, product ]); }); } }
State Management Techniques
1. Basic State Container
typescript
interface AppState { user: User; products: Product[]; loading: boolean; } @Injectable({ providedIn: 'root' }) export class AppStore { private state = new BehaviorSubject<AppState>({ user: null, products: [], loading: false }); state$ = this.state.asObservable(); select<K extends keyof AppState>(key: K): Observable<AppState[K]> { return this.state$.pipe(pluck(key)); } update(partialState: Partial<AppState>) { this.state.next({ ...this.state.value, ...partialState }); } }
2. Component State with Services
typescript
@Component({ selector: 'app-product-list', template: ` <div *ngFor="let product of products$ | async"> {{ product.name }} </div> `, providers: [ProductStateService] // Scoped service }) export class ProductListComponent { products$ = this.productState.products$; constructor(private productState: ProductStateService) {} }
Advanced Patterns
1. Redux-like Pattern with RxJS
typescript
interface Action { type: string; payload?: any; } @Injectable({ providedIn: 'root' }) export class Store { private stateSubject = new BehaviorSubject<any>(initialState); private actionSubject = new Subject<Action>(); state$ = this.stateSubject.asObservable(); constructor() { this.actionSubject.pipe( scan((state, action) => this.reducer(state, action), initialState) ).subscribe(this.stateSubject); } dispatch(action: Action) { this.actionSubject.next(action); } private reducer(state: any, action: Action) { switch (action.type) { case 'ADD_PRODUCT': return { ...state, products: [...state.products, action.payload] }; // More cases... } } }
2. Caching with RxJS
typescript
private cache = new Map<string, Observable<any>>(); getWithCache(url: string): Observable<any> { if (!this.cache.has(url)) { this.cache.set(url, this.http.get(url).pipe( shareReplay(1), // Cache and replay to late subscribers finalize(() => this.cache.delete(url)) // Clean up )); } return this.cache.get(url); }
When to Consider NgRx?
While RxJS is sufficient for many apps, consider NgRx when:
- Multiple components need the same state
- State transitions are complex
- You need time-travel debugging
- The app has many user interactions
- State persistence is required
Best Practices
- Single Source of Truth: Avoid duplicating state
- Immutable Updates: Always create new references
- Separation of Concerns: Keep business logic in services
- Unsubscribe: Avoid memory leaks
- Error Handling: Handle errors in state streams
Hands-On Exercise
Build a shopping cart state service with:
- Ability to add/remove items
- Quantity adjustments
- Total price calculation
- Persistence to localStorage
- Undo/redo functionality
What’s Next?
In Part 13, we’ll explore Angular Performance Optimization – techniques to make your apps blazing fast!