Menu

Advanced RxJS and State Management

Advanced RxJS and State Management

RxJS Operators in Depth

RxJS is the backbone of Angular's async programming. At the intermediate level, you used catchError and retry. At the advanced level, you need operators that handle real-world complexity — debouncing user input, cancelling stale requests, and combining multiple streams.

switchMap — cancel the previous request when a new one comes in:

This is the most important operator for search boxes. Without it, if a user types quickly, you can get responses arriving out of order.

// search.component.ts

import { Component, OnInit } from '@angular/core';

import { FormControl } from '@angular/forms';

import { CourseService } from './course.service';

import { switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({

selector: 'app-search',

template: `

<input [formControl]="searchControl" placeholder="Search courses...">

<ul>

<li *ngFor="let course of results">{{ course.name }}</li>

</ul>

`

})

export class SearchComponent implements OnInit {

searchControl = new FormControl('');

results: any[] = [];

constructor(private courseService: CourseService) {}

ngOnInit(): void {

this.searchControl.valueChanges.pipe(

debounceTime(300), // wait 300ms after the user stops typing

distinctUntilChanged(), // ignore if the value hasn't changed

switchMap(term => // cancel previous request, fire new one

this.courseService.search(term || '')

)

).subscribe(results => this.results = results);

}

}

combineLatest — react when any of multiple streams emits:

import { combineLatest } from 'rxjs';

// Fires whenever either the filter or the sort order changes

combineLatest([this.filter$, this.sortOrder$]).pipe(

switchMap(([filter, sort]) => this.courseService.getCourses(filter, sort))

).subscribe(courses => this.courses = courses);

**forkJoin — wait for multiple requests to all complete:**

import { forkJoin } from 'rxjs';

// Load courses and user profile at the same time, proceed when both are done

forkJoin({

courses: this.courseService.getCourses(),

profile: this.userService.getProfile()

}).subscribe(({ courses, profile }) => {

this.courses = courses;

this.profile = profile;

});

NgRx State Management

When your app grows beyond a few components, passing data around through services and @Input()/@Output() chains becomes hard to track. NgRx gives you a single, predictable store for all application state — modelled on Redux.

The four core pieces are: Store (holds the state), Actions (describe what happened), Reducers (update the state), and Selectors (read from the state).

Installing NgRx:

ng add @ngrx/store

ng add @ngrx/effects

ng add @ngrx/store-devtools

Defining actions:

// course.actions.ts

import { createAction, props } from '@ngrx/store';

import { Course } from './course.model';

export const loadCourses = createAction('[Course List] Load Courses');

export const loadCoursesSuccess = createAction(

'[Course List] Load Courses Success',

props<{ courses: Course[] }>()

);

export const loadCoursesFailure = createAction(

'[Course List] Load Courses Failure',

props<{ error: string }>()

);

Writing the reducer:

// course.reducer.ts

import { createReducer, on } from '@ngrx/store';

import { loadCourses, loadCoursesSuccess, loadCoursesFailure } from './course.actions';

import { Course } from './course.model';

export interface CourseState {

courses: Course[];

loading: boolean;

error: string | null;

}

const initialState: CourseState = {

courses: [],

loading: false,

error: null

};

export const courseReducer = createReducer(

initialState,

on(loadCourses, state => ({ ...state, loading: true, error: null })),

on(loadCoursesSuccess, (state, { courses }) => ({ ...state, courses, loading: false })),

on(loadCoursesFailure, (state, { error }) => ({ ...state, error, loading: false }))

);

Handling side effects with NgRx Effects:

Effects are where your API calls live. They listen for an action, call the API, then dispatch a success or failure action.

// course.effects.ts

import { Injectable } from '@angular/core';

import { Actions, createEffect, ofType } from '@ngrx/effects';

import { CourseService } from './course.service';

import { loadCourses, loadCoursesSuccess, loadCoursesFailure } from './course.actions';

import { switchMap, map, catchError } from 'rxjs/operators';

import { of } from 'rxjs';

@Injectable()

export class CourseEffects {

loadCourses$ = createEffect(() =>

this.actions$.pipe(

ofType(loadCourses),

switchMap(() =>

this.courseService.getCourses().pipe(

map(courses => loadCoursesSuccess({ courses })),

catchError(error => of(loadCoursesFailure({ error: error.message })))

)

)

)

);

constructor(private actions$: Actions, private courseService: CourseService) {}

}

Reading state in a component using Selectors:

// course-list.component.ts

import { Store } from '@ngrx/store';

import { loadCourses } from './course.actions';

import { selectAllCourses, selectLoading } from './course.selectors';

export class CourseListComponent implements OnInit {

courses$ = this.store.select(selectAllCourses);

loading$ = this.store.select(selectLoading);

constructor(private store: Store) {}

ngOnInit(): void {

this.store.dispatch(loadCourses());

}

}

<div *ngIf="loading$ | async">Loading...</div>

<ul>

<li *ngFor="let course of courses$ | async">{{ course.name }}</li>

</ul>