Commit 75b9dc26 authored by Julian's avatar Julian
Browse files

improve withLatestFrom

parent 36335da9
......@@ -5,7 +5,7 @@ Combination operators or creation functions enable us to process emissions
from different `Observables` and transform them into a single emission.
The resulting value is an array with the last emitted value of all included Observables.
In this very example, we will utilize the `forkJoin` operator.
In this very exercise, we will utilize the `forkJoin` operator.
On first sight it is a perfect match for combining HTTP Requests since it waits until all
combined operators `complete` before emitting a result.
......@@ -29,13 +29,12 @@ result$
})
```
The visual representation of the above example:
The gray boxes at the bottom of the operator scope symbolize a cache which stores the last emitted value of each included Observable.
![forkJoin http calls](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-forkJoin-http_michael-hladky.png)
_forkJoin http calls_
The gray boxes at the bottom of the operator scope symbolize a cache which stores the last emitted value of each included Observable.
If any of the sources raises an `error`, it gets forwarded, and the resulting Observable errors.
If any of the sources raises an `error`, it gets forwarded, and the resulting Observable errors.
![forkJoin error](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-forkJoin-error_michael-hladky.png)
_forkJoin error_
......
......@@ -58,7 +58,7 @@ export class BlogService {
this.state$.next({
...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, [newPost], 'id')
})
});
}, console.log);
}
......
......@@ -15,14 +15,16 @@ _current solution: instant updates visualized_
![desired solution: opt-in updates visualized](./assets/images/Reactive-architecture-and-ux-patterns_angular_opt-in-updates_michael-hladky.png)
_desired solution: opt-in updates visualized_
For this exercise we have to extend our `Component` with extra functionalities.
For this exercise we have to extend our `Component`.
```Typescript
// start.withLatestFrom.component.ts
@Component()
export class StartWithLatestFromComponent {
optInListClick$ = new Subject(); // performs the opt-in update on button click
numNewItems$: Observable<number>; // the derived number of new items available for display
feed$: Observable<BlogPost[]>; // the new feed to display. use blog$ and optIntListClick$ to calculate
feed$: Observable<BlogPost[]>; // the new feed to display. use blog$ and optInListClick$ to calculate
}
```
......@@ -30,4 +32,6 @@ export class StartWithLatestFromComponent {
Utilize the `withLatestFrom` combination operator and implement a _staging_ area for new arriving `BlogPost`.
Use the new introduced properties `optInListClick$`, `numNewItems$` and `feed$` to implement your solution. You should
end up replacing the view binding of `blog$` by `feed$`. Use `blog$` and `optInListClick$` to calculate the new feed.
end up replacing the view binding of `blog$` by `feed$`.
Use `blog$` and `optInListClick$` to compute the new feed.
# Opt-in updates - Solution
Staging new arriving data with `withLatestFrom`:
Let's take a look at what we have implemented.
At first, we used `combineLatest` in order to create an `Observable` source representing
the number of new items to opt-in to. This represents the indication for the user when new updates
are available for display.
**Component**
```Typescript
// solution.withLatestFrom.component.ts
blog$ = combineLatest([
this.blogService.posts$.pipe(filter(l => !!l.length)),
this.blogService.comments$.pipe(filter(l => !!l.length))
]).pipe(
map(([list, items]) => toBlogPosts(list, items)),
shareReplay(1)
);
feed$: Observable<BlogPost[]> = concat(
this.blog$.pipe(take(1)),
this.optInListClick$.pipe(
withLatestFrom(this.blog$),
map(([_, items]) => items)
),
shareReplay(1)
);
numNewItems$: Observable<number> = combineLatest([
this.blog$,
this.feed$
......@@ -29,6 +17,34 @@ numNewItems$: Observable<number> = combineLatest([
.pipe(
map(([a, b]) => Math.abs(a.length - b.length))
);
```
Then, we have built the opt-in update logic by using the `optInListClick$` as trigger
to emit the _latest_ value of `blog$` utilizing `withLatestFrom`.
```Typescript
// solution.withLatestFrom.component.ts
optInUpdate$: Observable<BlogPost[]> = this.optInListClick$.pipe(
withLatestFrom(this.blog$),
map(([_, items]) => items)
);
```
Finally, we built the `feed$`. We wanted `feed$` to start with the first value emitted by `blog$`. That is
why we have combined the `optInUpdate$` with the first emission of `blog$` using the `concat` creation
function.
```Typescript
// solution.withLatestFrom.component.ts
feed$: Observable<BlogPost[]> = concat(
this.blog$.pipe(take(1)),
this.optInUpdate$
).pipe(shareReplay(1));
```
Great! ... we need some text here :)
Great! We learned how to build a mechanism for user controlled opt-in updates with `withLatestFrom`.
# Opt-in updates - Theory
# withLatestFrom behavior and gotchas
The previous example showcased how to combine multiple ongoing `Observables` to one result. However, there are situations
where it is necessary to access the latest value from an `Observable` after a specific event occurs.
The previous example showcased how to combine multiple ongoing `Observables` to one result. However, there are
situations where it is necessary to access the latest value from an `Observable` after a specific event occurs.
`withLatestFrom` combines the last _emitted_ value of the a provided source `Observable` to an active stream of data.
`withLatestFrom` combines the last _emitted_ value of the provided source `Observable` to an active stream of data.
In this example we want to get the latest value of the `timer$` whenever a user clicks on the document.
We will utilize the `withLatestFrom` operator in this exercise to setup a _staging_ area for new incoming `BlogPost`
in order to achieve user controller opt-in updates for the view.
## Behavior
An example where the latest value of `b$` is logged after a user clicks on the document.
```Typescript
import { fromEvent, interval } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
const click$ = fromEvent(document, 'click'); // get the click event
const timer$ = interval(1000);
const result = click$.pipe(withLatestFrom(timer$));
const input$ = fromEvent(document, 'click'); // get the click event
const b$ = interval(1000);
const result = input$.pipe(withLatestFrom(b$));
result.subscribe(([clickEvent, latestTimerValue]) => console.log(latestTimerValue)); // logs the latest value of the timer
```
![The visual representation of this example](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-theory-example_michael-hladky.png)
_The visual representation of this example_
If the _outer_ `Observable`, you originally subscribed to, or the _inner_ `Observable` which you applied `withLatestFrom`
to raises an `error`, the end result will be an `error` too.
![withLatestFrom outer error](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-outer-error_michael-hladky.png)
_withLatestFrom outer error_
![withLatestFrom inner error](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-inner-error_michael-hladky.png)
_withLatestFrom inner error_
If the `Observable` within `withLatestFrom` raises a `completion` event, the stream will continue using the last emitted
value of the source.
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-outer-ongoing_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-outer-error_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-inner-error_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-outer-complete_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-inner-complete_michael-hladky.png)
_withLatestFrom inner complete_
Gotcha => startWith
## 💡 Gotcha(s)!
`withLatestFrom` will only emit values if the provided source ever emitted a value.
The following illustration visualizes this behavior. Even though `input$` emits `A` and `B`, the `subscriber` doesn't get
notified about the changes.
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_withLatestFrom-outer-ongoing_michael-hladky.png)
_withLatestFrom waits until at least one value was emitted_
......@@ -14,15 +14,13 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared';
</mat-form-field>
<button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<ng-container *ngIf="(numNewItems$ | async) as numItems">
<button mat-raised-button color="accent"
[disabled]="numItems === 0"
(click)="optInListClick$.next($event)">
New posts: ({{(
numItems
)}})
</button>
</ng-container>
<button mat-raised-button color="accent"
[disabled]="(numNewItems$ | async) === 0"
(click)="optInListClick$.next($event)">
New posts: ({{(
numNewItems$ | async
)}})
</button>
<div *ngIf="feed$ | async as blog">
<mat-list>
......@@ -47,14 +45,16 @@ export class SolutionWithLatestFromComponent {
shareReplay(1)
);
optInUpdate$: Observable<BlogPost[]> = this.optInListClick$.pipe(
withLatestFrom(this.blog$),
map(([_, items]) => items)
);
feed$: Observable<BlogPost[]> = concat(
this.blog$.pipe(take(1)),
this.optInListClick$.pipe(
withLatestFrom(this.blog$),
map(([_, items]) => items)
),
shareReplay(1)
);
this.optInUpdate$
).pipe(shareReplay(1));
numNewItems$: Observable<number> = combineLatest([
this.blog$,
this.feed$
......
......@@ -15,15 +15,12 @@ import { BlogPost, toBlogPosts } from 'shared';
</mat-form-field>
<button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<ng-container *ngIf="(numNewItems$ | async) as numItems">
<button mat-raised-button color="accent"
[disabled]="numItems === 0"
(click)="optInListClick$.next($event)">
New posts: ({{(
numItems
)}})
</button>
</ng-container>
<button mat-raised-button color="accent"
(click)="optInListClick$.next($event)">
New posts: ({{(
numNewItems$ | async
)}})
</button>
<div *ngIf="blog$ | async as blog">
<mat-list>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment