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!
