Download as pdf or txt
Download as pdf or txt
You are on page 1of 39

Data Composition

with RxJS
(Links to slides and code provided)

Deborah Kurata
Developer | Pluralsight Author | Consultant | MVP | GDE
deborahkurata
We’ll merge
the streams!

Screenshot of a scene from the movie “GhostBusters” Burbank, CA: RCA/Columbia Pictures Home Video, 1984.
Declarative approach to
● Collecting
● Composing
● Caching
using RxJS streams
with NO subscriptions
Sample Application -> Reactive Development
Collecting
Current Pattern for Retrieving Data
@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(console.log),
catchError(this.handleError)
);
}
}
Declarative Pattern for Retrieving Data
@Injectable({ providedIn: 'root' })
export class ProductService {
private productsUrl = 'api/products';

products$ = this.http.get<Product[]>(this.productsUrl)
.pipe(
tap(console.log),
catchError(this.handleError)
);

constructor(private http: HttpClient) { }


}
Component
export class ProductListComponent {

products$ = this.productService.products$
.pipe(
catchError(error => {
this.errorMessage = error;
return of(null);
}));

constructor(private productService: ProductService) { }


}
Template
<div *ngIf="products$ | async as products">
<button type='button'
*ngFor='let product of products'>
{{ product.productName }} ({{ product.category }})
</button>
</div>
Component
@Component({
selector: 'pm-product-list',
templateUrl: './product-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})

● OnPush improves performance by minimizing change detection cycles


● With OnPush, the component is only checked for changes when:
○ @Input property changes
○ An event emits
○ A bound observable emits
More Declarative, More Reactive

Compose Leverage Improve React to


streams RxJS change user
operators detection actions
Composing
Declaratively Define the Category Stream
@Injectable({ providedIn: 'root' })
export class ProductCategoryService {
private productCategoriesUrl = 'api/productCategories';

productCategories$ = this.http.get<ProductCategory[]>
(this.productCategoriesUrl)
.pipe(
tap(console.log),
catchError(this.handleError)
);

constructor(private http: HttpClient) { }


}
RxJS Creation Function: combineLatest
productsWithCategory$ = combineLatest(
this.products$,
this.ProductCategoryService.productCategories$
) [ Products[], ProductCategories[] ]

● Combines multiple streams into a new stream


● Uses the latest emitted values of each of its input streams
● Waits for each input stream to emit at least once before it starts emitting
● Emits an array of values, in order, one for each input stream
Combining the Streams
productsWithCategory$ = combineLatest(
this.products$,
this.productCategoryService.productCategories$
).pipe(
map(([products, categories]) =>
products.map(
p =>
({ ...p, Array
category: categories.find(c =>
destructuring
p.categoryId === c.id).name
} as Product) // <-- note the type here!
)
)
);
Combining the Streams
productsWithCategory$ = combineLatest(
this.products$,
Array map
this.productCategoryService.productCategories$
method
).pipe(
Spread operator copies
map(([products, categories]) =>
over the properties Find the category name by
products.map(
p => category id
({ ...p,
category: categories.find(c =>
p.categoryId === c.id).name
} as Product)
)
) Define the resulting type
);
Component
export class ProductListComponent implements OnInit {

products$ = this.productService.productsWithCategory$
.pipe(
catchError(error => {
this.errorMessage = error;
return of(null);
}));

constructor(private productService: ProductService) { }


}
Reacting to
Actions
Reacting to Actions

Emit a value
Create an action Combine action
every time an
stream and data streams
action occurs
Creating an Action Stream
private productSelectedAction = new Subject<number>();

productSelectedAction$ = this.productSelectedAction.asObservable();

● Use Subject/BehaviorSubject ● Observable


● Special type of Observable ● Observer
● Multicast ○ next, error, complete
Combining Data and Action Streams
[{saw},
{rake},
{axe}]

1 3 14

combineLatest(data$, action$)

[[{saw}, [[{saw}, [[{saw},


{rake}, {rake}, {rake},
{axe}], {axe}], {axe}],
1] 3] 14]
Combining Data and Action Streams
selectedProduct$ = combineLatest(
this.productSelectedAction,
this.productsWithCategory$
).pipe(
map(([selectedProductId, products]) =>
products.find(product => product.id === selectedProductId)
) Array To find the selected product,
); destructuring we need both streams
Emitting a Value on Action
onSelected(productId: number): void {
this.productService.changeSelectedProduct(productId);
}

changeSelectedProduct(selectedProductId: number | null): void {


this.productSelectedAction.next(selectedProductId);
}
Emitting Re-executes the Pipeline
[{saw},
{rake},
{axe}]

1 14

combineLatest(data$, action$)

[ [
[{saw}, [{saw},
{rake}, {rake},
{axe}], {axe}],
1] 14]
Caching
Cache the Array?
@Injectable({ providedIn: 'root' })
export class ProductService {
private productsUrl = 'api/products';
products: Product[];

products$ = this.http.get<Product[]>(this.productsUrl)
.pipe(
map(data => this.products = data),
catchError(this.handleError)
);

constructor(private http: HttpClient) { }


}
RxJS operator: shareReplay
products$ = this.http.get<Product[]>(this.productsUrl)
.pipe(
tap(console.log),
shareReplay(),
catchError(this.handleError)
);

● Shares an Observable with all subscribers


● Replays the specified number of entries upon subscription
● Remains active, even if there are no current subscribers
Observable
All the Things
Observable All the Things
Component
pageTitle$ = this.product$
.pipe(
map((p: Product) =>
p ? `Product Detail for: ${p.productName}` : null)
);

productSuppliers$ = this.productService.selectedProductSuppliers$
.pipe(
catchError(error => {
this.errorMessage = error;
return of(null);
}));
We’ll merge
the streams!

Screenshot of a scene from the movie “GhostBusters” Burbank, CA: RCA/Columbia Pictures Home Video, 1984.
Component
vm$ = combineLatest(
[this.product$,
this.productSuppliers$,
this.pageTitle$])
Array
.pipe(
destructuring
filter(([product]) => !!product),
map(([product, productSuppliers, pageTitle]) =>
({ product, productSuppliers, pageTitle }))
);
*Thanks to Sander Elias for showing me this technique
Template
<div *ngIf="vm$ | async as vm">
<div>{{vm.pageTitle}}</div>
...
<div>Name:</div>
<div>{{vm.product.productName}}</div>
...
<tr *ngFor="let supplier of vm.productSuppliers">
...
Declarative, Reactive Streams
CONS PROS
● Picking the right operator is not ● Composable streams
always easy ● Leverages power of RxJS
● Difficult to debug Observables operators
● Sharing Observables is easy
(cache)
● Effectively react to actions
● OnPush for improved
performance
Data Composition with RxJS

Slides:
http://bit.ly/deborahk-ngconf2019
Code:
https://github.com/DeborahK/Angular-DD
Twitter:
deborahkurata

You might also like