4 �RxJS Pipelines�for the Real World
Deborah Kurata
Developer | Author | MVP | GDE
@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
RxJS Pipelines
Retrieve Related Data Pipeline
Lookup Reference Property Pipeline
Grouping Pipeline
Autocomplete Pipeline
@deborahkurata
Tip:
What do you have?
What do you want?
When do you want it?
@deborahkurata
Retrieve Related Data Pipeline
@deborahkurata
Retrieve Related Data Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Retrieve Related Data Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Retrieve Related Data Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Tip:
To respond to an action, use a Subject or BehaviorSubject
@deborahkurata
Subject / BehaviorSubject
private userSubject = new Subject<string>();
enteredUser$ = this.userSubject.asObservable();
private userSubject = new BehaviorSubject<string>('');
enteredUser$ = this.userSubject.asObservable();
@deborahkurata
Retrieve Related Data Pipeline
What about exception handling?
What if the userName isn't found?
Q
postsForUser$ = this.enteredUser$.pipe(
switchMap(userName =>
this.http.get<User[]>(`${this.usersUrl}?userName=^${userName}$`)),
switchMap(users =>
this.http.get<Post[]>(`${this.postsUrl}?userId=^${users[0].id}$`))
);
Uses regEx
Returns an array
First user
@deborahkurata
Tip:
Refactor code into manageable and reusable pieces
@deborahkurata
Retrieve Related Data Pipeline
getUserId(userName: string): Observable<number> {
return this.http.get<User[]>(`${this.usersUrl}?userName=^${userName}$`).pipe(
catchError(this.handleError),
map(users => (users.length === 0) ? 0 : users[0].id)
);
}
private getPostsForUser(userId: number): Observable<Post[]> {
return this.http.get<Post[]>(`${this.postsUrl}?userId=^${userId}$`).pipe(
catchError(this.handleError)
);
}
@deborahkurata
Retrieve Related Data Pipeline
postsForUser$ = this.enteredUser$.pipe(
switchMap(userName => this.userService.getUserId(userName)),
switchMap(userId => this.getPostsForUser(userId))
);
getUserId(userName: string): Observable<number> {
return this.http.get<User[]>(`${this.usersUrl}?userName=^${userName}$`).pipe(
catchError(this.handleError),
map(users => (users.length === 0) ? 0 : users[0].id)
);}
private getPostsForUser(userId: number): Observable<Post[]> {
return this.http.get<Post[]>(`${this.postsUrl}?userId=^${userId}$`).pipe(
catchError(this.handleError)
);}
@deborahkurata
Retrieve Related Data Pipeline
@deborahkurata
Lookup Reference Property Pipeline
@deborahkurata
Lookup Reference Property Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Lookup Reference Property Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Lookup Reference Property Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Tip:
To work with multiple streams, use a combination operator
@deborahkurata
Lookup Reference Property Pipeline
What about exception handling?
How do we reuse the retrieved categories?
Q
postsWithCategory$ = combineLatest([
this.http.get<Post[]>(this.postsUrl),
this.http.get<PostCategory[]>(this.postCategoriesUrl)
]);
@deborahkurata
Lookup Reference Property Pipeline
allPosts$ = this.http.get<Post[]>(this.postsUrl).pipe(
catchError(this.handleError)
);
allCategories$ = this.http.get<PostCategory[]>(this.catUrl).pipe(
catchError(this.handleError),
shareReplay(1)
);
postsWithCategory$ = combineLatest([
this.allPosts$,
this.categoryService.allCategories$
]) ...;
@deborahkurata
Lookup Reference Property Pipeline
postsWithCategory$ = combineLatest([
this.allPosts$,
this.categoryService.allCategories$
]).pipe(
map(([posts, cats]) => posts.map(post => ({
...post,
category: cats.find(c => post.categoryId === c.id)?.name
}) as Post))
);
// Observable<Post[]>
Array destructuring
Array map
Strong typing
@deborahkurata
Lookup Reference Property Pipeline
What if the id isn't found?
What if there are multiple lookups?
How do we reuse this logic?
Q
postsWithCategory$ = combineLatest([
this.allPosts$,
this.categoryService.allCategories$
]).pipe(
map(([posts, cats]) => posts.map(post => ({
...post,
category: cats.find(c => post.categoryId === c.id)?.name
}) as Post))
);
// Observable<Post[]>
@deborahkurata
Lookup Reference Property Pipeline
postsWithCategory$ = combineLatest([
this.allPosts$,
this.categoryService.allCategories$
]).pipe(
map(([posts, cat]) => this.mapCategories(posts, cat)),
);
mapCategories(posts: Post[], cat: PostCategory[]): Post[] {
return posts.map(post => ({
...post,
category: cat.find(c => post.categoryId === c.id)?.name
}) as Post);
}
@deborahkurata
Lookup Reference Property Pipeline
@deborahkurata
Grouping Pipeline
@deborahkurata
Grouping Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Grouping Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Grouping Pipeline
postsGroupedByCategory$ = this.postsWithCategory$.pipe(
concatAll(),
groupBy(post => post.categoryId, post => post),
mergeMap(group => zip(of(group.key), group.pipe(toArray()))),
toArray()
);
Emits each array element
Key selector
Element selector
@deborahkurata
Grouping Pipeline
postsGroupedByCategory$ = this.postsWithCategory$.pipe(
concatAll(),
groupBy(post => post.categoryId, post => post),
mergeMap(group => zip(of(group.key), group.pipe(toArray()))),
toArray()
);
For each group, emits one tuple with the id and post[]
Emits one array with all tuples
@deborahkurata
Grouping Pipeline
@deborahkurata
Grouping Pipeline
@deborahkurata
Autocomplete Pipeline
@deborahkurata
Autocomplete Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Autocomplete Pipeline
What do we have?
What do we want?
When do we want it?
@deborahkurata
Autocomplete Pipeline
What do we want? | When do we want it?
Type: Filter the list
Click: Open the list
Focus: Open the list
Click x: Clear and open the list
Action -> Subject/BehaviorSubject
@deborahkurata
Autocomplete Pipeline
focus$ = new Subject<string>();
click$ = new Subject<string>();
clear$ = new Subject<string>();
<input type="text"
[(ngModel)]="selectedCategory"
(selectItem)="categorySelected($event.item)"
[ngbTypeahead]="search"
...
(focus)="focus$.next($any($event).target.value)"
(click)="click$.next($any($event).target.value)" />
<button type="button"
(click)="onClear()">
<i class="fa fa-times"></i>
</button>
@deborahkurata
Autocomplete Pipeline
focus$ = new Subject<string>();
click$ = new Subject<string>();
clear$ = new Subject<string>();
search = (text$: Observable<string>) => {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clicksClosed$ = this.click$.pipe(
filter(() => !this.instance.isPopupOpen()));
const operations$ = merge(debouncedText$, clicksClosed$, this.focus$, this.clear$);
return combineLatest([
operations$,
this.categories$]).pipe(
map(([txt, cat]) =>
txt === '' ? categories : cat.filter(c => new RegExp(`^${txt}`, 'i').test(c.name)))
);
}
@deborahkurata
Autocomplete Pipeline
focus$ = new Subject<string>();
click$ = new Subject<string>();
clear$ = new Subject<string>();
search = (text$: Observable<string>) => {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clickToOpen$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const operations$ = merge(debouncedText$, clicksClosed$, this.focus$, this.clear$);
return combineLatest([
operations$,
this.categories$]).pipe(
map(([txt, cat]) =>
txt === '' ? categories : cat.filter(c => new RegExp(`^${txt}`, 'i').test(c.name)))
);
}
@deborahkurata
Autocomplete Pipeline
focus$ = new Subject<string>();
click$ = new Subject<string>();
clear$ = new Subject<string>();
search = (text$: Observable<string>) => {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clickToOpen$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const operations$ = merge(debouncedText$, clickToOpen$, this.focus$, this.clear$);
return combineLatest([
operations$,
this.categories$]).pipe(
map(([txt, cat]) =>
txt === '' ? categories : cat.filter(c => new RegExp(`^${txt}`, 'i').test(c.name)))
);
}
@deborahkurata
Autocomplete Pipeline
focus$ = new Subject<string>();
click$ = new Subject<string>();
clear$ = new Subject<string>();
search = (text$: Observable<string>) => {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clickToOpen$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const operations$ = merge(debouncedText$, clickToOpen$, this.focus$, this.clear$);
return combineLatest([
operations$,
this.categories$]).pipe(
map(([txt, cat]) =>
txt === '' ? categories : cat.filter(c => new RegExp(`^${txt}`, 'i').test(c.name)))
);
}
@deborahkurata
Autocomplete Pipeline
focus$ = new Subject<string>();
click$ = new Subject<string>();
clear$ = new Subject<string>();
search = (text$: Observable<string>) => {
const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged());
const clickToOpen$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
const operations$ = merge(debouncedText$, clickToOpen$, this.focus$, this.clear$);
return combineLatest([
operations$,
this.categories$]).pipe(
map(([txt, cat]) =>
txt === '' ? categories : cat.filter(c => new RegExp(`^${txt}`, 'i').test(c.name)))
);
}
case insensitive check
@deborahkurata
Autocomplete Pipeline
@deborahkurata
4 �RxJS Pipelines
@deborahkurata
https://github.com/DeborahK/Angular-Posts
@deborahkurata