RxJS Patterns
Deborah Kurata
Developer | Author | MVP | GDE
@deborahkurata
RxJS Patterns
Declarative Data Access Pattern
Retrieve on Action Pattern
Shape on Action Pattern
Retrieve Related Data Pattern
@deborahkurata
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
Tip 1:
What do you have?
What do you want?
When do you want it?
@deborahkurata
Sample Application
What do we have?
What do we want?
When do we want it?
@deborahkurata
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
Tip 2:
Ensure each Observable is subscribed
Ensure each subscription is unsubscribed
@deborahkurata
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
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
Component
ngOnInit(): void {
this.sub = this.productService.getProducts().subscribe(
products => this.products = products
);
}
ngOnDestroy(): void {
this.sub.unsubscribe();
}
products$ = this.productService.products$;
@deborahkurata
Tip 3:
Use the async pipe
@deborahkurata
Template
<div *ngIf="products$ | async as products">
<button type='button'
*ngFor='let product of products'>
{{ product.productName }} ({{ product.category }})
</button>
</div>
@deborahkurata
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
Declarative Data Access Pattern
[{saw},�{rake}, {axe}]
Data Stream
@deborahkurata
"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
Tip 4:
To respond to an action, use a Subject or BehaviorSubject
@deborahkurata
Subject / BehaviorSubject
private categorySubject = new Subject<number>();
categorySelectedAction$ = this.categorySubject.asObservable();
private categorySubject = new BehaviorSubject<number>(1);
categorySelectedAction$ = this.categorySubject.asObservable();
@deborahkurata
Emitting a Value
selectedCategoryChanged(categoryId: number): void {
this.categorySubject.next(categoryId);
}
private categorySubject = new Subject<number>();
categorySelectedAction$ = this.categorySubject.asObservable();
@deborahkurata
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
Tip 5:
Leverage your IDE
@deborahkurata
Leverage Your IDE
@deborahkurata
Tip 6:
To subscribe to an inner Observable and
flatten the result, use a higher-order mapping operator
(aka a flattening operator)
@deborahkurata
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
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
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
Retrieve on Action Pattern
@deborahkurata
Retrieve on Action Pattern
[{saw},�{drill}, {level}]
c42
c15
Action Stream
Result Stream
[{rake},�{mower}, {cart}]
switchMap(catId => ...)
@deborahkurata
Shape on Action Pattern
What do we have?
What do we want?
When do we want it?
@deborahkurata
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
Tip 7:
To work with multiple streams, use a combination operator
@deborahkurata
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
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
Shape on Action Pattern
{saw}
p1
p3
Action Stream
Result Stream
{axe}
combineLatest([data$, action$])
[{saw},�{rake}, {axe}]
Data Stream
@deborahkurata
Retrieve Related Data Pattern
What do we have?
What do we want?
When do we want it?
@deborahkurata
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
Retrieve Related Data Pattern (Many)
@deborahkurata
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
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
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
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
Links
@deborahkurata
https://github.com/DeborahK/� Angular-ActionStreams
https://github.com/DeborahK/toh
@deborahkurata