Part 12: State Management with RxJS – Reactive Application State

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

  1. Single Source of Truth: Avoid duplicating state
  2. Immutable Updates: Always create new references
  3. Separation of Concerns: Keep business logic in services
  4. Unsubscribe: Avoid memory leaks
  5. Error Handling: Handle errors in state streams

Hands-On Exercise

Build a shopping cart state service with:

  1. Ability to add/remove items
  2. Quantity adjustments
  3. Total price calculation
  4. Persistence to localStorage
  5. Undo/redo functionality

What’s Next?

In Part 13, we’ll explore Angular Performance Optimization – techniques to make your apps blazing fast!

Leave a Comment

Your email address will not be published. Required fields are marked *