1 of 41

RxJS Patterns

Deborah Kurata

Developer | Author | MVP | GDE

@deborahkurata

2 of 41

RxJS Patterns

Declarative Data Access Pattern

Retrieve on Action Pattern

Shape on Action Pattern

Retrieve Related Data Pattern

@deborahkurata

3 of 41

Deborah Kurata

Developer

Pluralsight Author

Angular Getting Started

Angular Reactive Forms

Angular Routing

RxJS in Angular: Reactive Development

Angular NgRx: Getting Started

C# OOP & Best Practices

Microsoft Most Valuable Professional (MVP)

Google Developer Expert (GDE)

@deborahkurata

4 of 41

Tip 1:

What do you have?

What do you want?

When do you want it?

@deborahkurata

5 of 41

Sample Application

What do we have?

What do we want?

When do we want it?

@deborahkurata

6 of 41

Classic Pattern for Retrieving Data: Service

@Injectable({ providedIn: 'root' })

export class ProductService {

private productsUrl = 'api/products';� constructor(private http: HttpClient) { }

getProducts(): Observable<Product[]> {

return this.http.get<Product[]>(this.productsUrl)

.pipe(

tap(data => console.log(JSON.stringify(data))),

catchError(this.handleError)

);

}

}

@deborahkurata

7 of 41

Tip 2:

Ensure each Observable is subscribed

Ensure each subscription is unsubscribed

@deborahkurata

8 of 41

Classic Pattern for Retrieving Data: Component

export class ProductListComponent implements OnInit, OnDestroy {

products: Product[];

sub: Subscription;

constructor(private productService: ProductService) { }

ngOnInit(): void {

this.sub = this.productService.getProducts().subscribe(

products => this.products = products

);

}

ngOnDestroy(): void {

this.sub.unsubscribe();

}

}

@deborahkurata

9 of 41

Service

getProducts(): Observable<Product[]> {

return this.http.get<Product[]>(this.productsUrl)

.pipe(

tap(data => console.log(JSON.stringify(data))),

catchError(this.handleError)

);

}

products$ = this.http.get<Product[]>(this.productsUrl)

.pipe(

tap(data => console.log(JSON.stringify(data))),

catchError(this.handleError)

);

@deborahkurata

10 of 41

Component

ngOnInit(): void {

this.sub = this.productService.getProducts().subscribe(

products => this.products = products

);

}

ngOnDestroy(): void {

this.sub.unsubscribe();

}

products$ = this.productService.products$;

@deborahkurata

11 of 41

Tip 3:

Use the async pipe

@deborahkurata

12 of 41

Template

<div *ngIf="products$ | async as products">

<button type='button'

*ngFor='let product of products'>

{{ product.productName }} ({{ product.category }})

</button>

</div>

@deborahkurata

13 of 41

Declarative Data Access Pattern

<div *ngIf="products$ | async as products">

<button type='button'

*ngFor='let product of products'>

{{ product.productName }} ({{ product.category }})

</button>

</div>

products$ = this.productService.products$;

products$ = this.http.get<Product[]>(this.url)

.pipe(

tap(data => console.log(data)),

catchError(this.handleError)

);

@deborahkurata

14 of 41

Declarative Data Access Pattern

[{saw},�{rake}, {axe}]

Data Stream

@deborahkurata

15 of 41

"Passing" Data

What do we have?

What do we want?

When do we want it?

products$=this.http.get<Product[]>(`${this.url}?cat=${catId}`)

.pipe(

tap(data => console.log(data)),

catchError(this.handleError)

);

@deborahkurata

16 of 41

Tip 4:

To respond to an action, use a Subject or BehaviorSubject

@deborahkurata

17 of 41

Subject / BehaviorSubject

private categorySubject = new Subject<number>();

categorySelectedAction$ = this.categorySubject.asObservable();

private categorySubject = new BehaviorSubject<number>(1);

categorySelectedAction$ = this.categorySubject.asObservable();

@deborahkurata

18 of 41

Emitting a Value

selectedCategoryChanged(categoryId: number): void {

this.categorySubject.next(categoryId);

}

private categorySubject = new Subject<number>();

categorySelectedAction$ = this.categorySubject.asObservable();

@deborahkurata

19 of 41

Retrieve on Action Pattern

products$ = this.categorySelectedAction$.pipe(

???map(catId => this.http.get<Product[]>(`${this.url}?cat=${catId}`))

.pipe(

tap(data => console.log(data)),

catchError(this.handleError)

));

private categorySubject = new Subject<number>();

categorySelectedAction$ = this.categorySubject.asObservable();

@deborahkurata

20 of 41

Tip 5:

Leverage your IDE

@deborahkurata

21 of 41

Leverage Your IDE

@deborahkurata

22 of 41

Tip 6:

To subscribe to an inner Observable and

flatten the result, use a higher-order mapping operator

(aka a flattening operator)

@deborahkurata

23 of 41

Higher-Order Mapping Operators

Automatically subscribe to the inner Observable

Flatten the resulting Observable

Returning Observable<T> not Observable<Observable<T>>

Automatically unsubscribe from the inner Observable

@deborahkurata

24 of 41

Higher-Order Mapping Operators

switchMap

Stops the current operation and performs the new operation

concatMap

Performs each operation one at a time, in order

mergeMap

Performs each operation concurrently

@deborahkurata

25 of 41

Retrieve on Action Pattern

private categorySubject = new Subject<number>();

categorySelectedAction$ = this.categorySubject.asObservable();

products$ = this.categorySelectedAction$.pipe(

switchMap(catId=>this.http.get<Product[]>(`${this.url}?cat=${catId}`))

.pipe(

tap(data => console.log(data)),

catchError(this.handleError)

));

@deborahkurata

26 of 41

Retrieve on Action Pattern

@deborahkurata

27 of 41

Retrieve on Action Pattern

[{saw},�{drill}, {level}]

c42

c15

Action Stream

Result Stream

[{rake},�{mower}, {cart}]

switchMap(catId => ...)

@deborahkurata

28 of 41

Shape on Action Pattern

What do we have?

What do we want?

When do we want it?

@deborahkurata

29 of 41

Shape on Action Pattern

products$ = this.categorySelectedAction$.pipe(

switchMap(catId=>this.http.get<Product[]>(`${this.url}?cat=${catId}`))

.pipe(

tap(data => console.log(data)),

catchError(this.handleError)

));

private productSelectedSubject = new Subject<number>();

productSelectedAction$ = this.productSelectedSubject.asObservable();

@deborahkurata

30 of 41

Tip 7:

To work with multiple streams, use a combination operator

@deborahkurata

31 of 41

Combination Operators

combineLatest

Emits a combined value when any of the Observables emit

Won't emit until all Observables have emitted at least once

merge

Emits the one value when any of the Observables emit

forkJoin

When all Observables complete, emit the last value from each Observable into an array

@deborahkurata

32 of 41

Shape on Action Pattern

products$ = this.categorySelectedAction$.pipe(

switchMap(catId=>this.http.get<Product[]>(`${this.url}?cat=${catId}`))

.pipe(...));

selectedProduct$ = combineLatest([

this.products$,

this.productSelectedAction$

]).pipe(

map(([products, selectedProductId]) =>

products.find(product => product.id === selectedProductId)

));

private productSelectedSubject = new Subject<number>();

productSelectedAction$ = this.productSelectedSubject.asObservable();

@deborahkurata

33 of 41

Shape on Action Pattern

{saw}

p1

p3

Action Stream

Result Stream

{axe}

combineLatest([data$, action$])

[{saw},�{rake}, {axe}]

Data Stream

@deborahkurata

34 of 41

Retrieve Related Data Pattern

What do we have?

What do we want?

When do we want it?

@deborahkurata

35 of 41

Retrieve Related Data Pattern (One)

selectedProduct$ = this.productSelectedAction$.pipe(

switchMap(id=>this.http.get<Product>(`${this.url}/${id}`))

.pipe(

tap(data => console.log(data)),

catchError(this.handleError)

));

productSupplier$ = this.selectedProduct$

.pipe(

switchMap(product =>

this.http.get<Supplier>(`${this.sUrl}/${product.supplierId}`))

);

@deborahkurata

36 of 41

Retrieve Related Data Pattern (Many)

@deborahkurata

37 of 41

Retrieve Related Data Pattern (Many)*

productSuppliers$ = this.selectedProduct$

.pipe(

switchMap(product =>

from(product.supplierIds)

.pipe(

mergeMap(supplierId =>

this.http.get<Supplier>(`${this.sUrl}/${supplierId}`)),

toArray())

)

);

*Not the best implementation

@deborahkurata

38 of 41

Retrieve Related Data Pattern (Many)*

productSuppliers$ = this.selectedProduct$

.pipe(

switchMap(product =>

forkJoin(product.supplierIds.map(supplierId =>

this.http.get<Supplier>(`${this.sUrl}/${supplierId}`)))

));

*Recommended implementation

@deborahkurata

39 of 41

Retrieve Related Data Pattern

{Saws R Us}

Data Stream

Result Stream

[{Saws R Us},�{Blade Runner}]

{saw}

forkJoin()

Product Stream

{Blade Runner}

Data Stream

@deborahkurata

40 of 41

RxJS Patterns

Declarative Data Access Pattern

Retrieve data

Retrieve on Action Pattern

Retrieve based on user selection, paging, etc

Shape on Action Pattern

Filter, map, transform an Observable on user selection, etc

Retrieve Related Data Pattern

Retrieve data and use id(s) to retrieve related data

@deborahkurata

41 of 41

Links

@deborahkurata

https://github.com/DeborahK/� Angular-ActionStreams

https://github.com/DeborahK/toh

@deborahkurata