Commit cd158540 authored by Julian's avatar Julian
Browse files

improve combineLatest exercises

parent 87680965
# Combining ongoing Observables - Exercise
## Intro
Processing HTTP calls directly in the component without any caching approaches will quickly result in _over-fetching_.
We request data from the server more often than we need to.
_Over-fetching HTTP requests visualized_
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_over-fetching_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_http-caching_michael-hladky.png)
## Intro
As processing HTTP calls directly in the component we run into "over-fetching" of data.
Over-fetching means we request data from the server more often than we need to.
This is a result of wrong state-management. To face this problem, we need to store the results of the HTTP Requests in
a shared cache. We can put this behavior inside a `Service` in order to share our state accross multiple `Components`.
This is a result of wrong state-management.
We need the result of the HTTP request on a global level in the service.
From there, HTTP result can be cached, share with multiple components and replayed from the cache if needed.
To do so, we implemented a BehaviorSubject in our service as well as
some new methods to update the services state.
_Shared data store visualized_
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_http-caching_michael-hladky.png)
For this exercise we introduce a very basic HTTP cache solution by using a `BehaviorSubject` as a shared data store. Instead
of binding `Components` directly to HTTP Requests, we will feed the data store with the results and provide
single shared `Observables` mapped from the data store.
**Service**
```Typescript
......@@ -23,37 +26,40 @@ private readonly state$ = new BehaviorSubject<BlogServiceState>({
comments: [] as Comment[]
});
readonly posts$ = this.state$.pipe(map(s => s.posts));
readonly comments$ = this.state$.pipe(map(s => s.comments));
readonly posts$ = this.state$.pipe(map(s => s.posts)); // shared and replayed observable posts
readonly comments$ = this.state$.pipe(map(s => s.comments)); // shared and replayed observable comments
// ...
fetchPosts() {
this.httpGetPosts()
.subscribe(posts => {
this.state$.next({
...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, posts, 'id')
});
});
this.httpGetPosts()
.subscribe(posts => {
this.state$.next({
...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, posts, 'id')
});
});
}
fetchComments() {
this.httpGetComments()
.subscribe(comments => {
this.state$.next({
...this.state$.getValue(),
comments: upsertEntities(this.state$.getValue().comments, comments, 'id')
});
});
this.httpGetComments()
.subscribe(comments => {
this.state$.next({
...this.state$.getValue(),
comments: upsertEntities(this.state$.getValue().comments, comments, 'id')
});
});
}
addPost(post: Pick<Post, 'title'>) {
this.httpPostPost(post)
.subscribe((v) => {
console.log(v);
// this.fetchPosts();
}, console.log);
this.httpPostPost(post)
.subscribe((newPost) => {
console.log('saved ', newPost , 'to the server');
this.state$.next({
...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, [newPost], 'id')
})
}, console.log);
}
```
......@@ -72,17 +78,19 @@ constructor(...) {
## Exercise
Use the properties `posts$` and `comments$` instead of `httpGetPosts` and `httpGetComments`.
If you click the "add post" button you will notice the calculated blog posts don't emit anymore.
Eliminate the HTTP requests from the `Component` by making use of the shared Observables `posts$` and `comments$`.
Even though the data in our store gets updated properly, the `Component` won't be able to display the list of
`BlogPost` anymore.
This is because `posts$` and `comments$` do not `complete`. Since `forkJoin` relies on its sources to complete, it won't
emit any value.
This is because our new streams do not complete anymore, but forkJoin needs all included Observables to complete to emit a value.
We need to replace `forkJoin` with an operator that matches the new requirements.
We need to replace `forkJoin` with an operator that allows to process the values of running Observables.
In this case `combineLatest` is a perfect match.
`combineLatest` is perfectly suited for this case.
Use it and see if the list of BlogPosts renders now.
Try adding a new `Post` using the "Add Post" button. ??
......
# Combining ongoing Observables - Solution
Combining the http calls with the `combineLatest` operator:
Combining the http requests with the `combineLatest` operator:
**Component**
```Typescript
......@@ -15,4 +15,5 @@ blog$: Observable<BlogPost[]> = combineLatest([
);
```
With this setup we can process the list of blog posts whenever the posts, or the comments change.
Great, `combineLatest` now enables us to handle the latest results of multiple **ongoing** `Observables`.
With this setup in place, we basically eliminated _over-fetching_. The list of `BlogPost` can be updated whenever the `posts$`, **or** the `comments$` change.
# `combineLatest` creation function - Theory
In the previous example we learned how to combine multiple HTTP Requests into one stream with `forkJoin`.
Often operators are special forms or sub forms of other operators.
If we take a look at the overview of combination patterns we realize that 2 of the look similar, forkJoin and combine.
If we take a look at the overview of combination patterns we realize that tow of them look very similar, forkJoin and combine.
They both process the values of the included Observables together, but `combineLatest` in comparison to `forkJoin` allows us to
process **every new value** with the latest values of the other included Observables.
Both combine the values of the source Observables to one emission, but `combineLatest` in comparison to `forkJoin` does not rely
on all sources to complete. Thus allowing us to process ongoing Observables. If any of the sources emits **a new value**, the
result will update to the **latest values** of each source.
![Combination pattern combine and forkJoin](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-forkJoin-vs-combine_michael-hladky.png)
An example of processing ongoing Observables with `combineLates` looks like that:
An example of processing ongoing Observables with `combineLatest`:
```Typescript
import { interval, combineLatest } from "rxjs";
import { map } from "rxjs/operators";
import { interval, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
const source1$ = interval(1000);
const source1$ = interval(1000); // ongoing, never completing source
const source2$ = interval(1000).pipe(map(i => i * 10));
const result$ = combineLatest([source1$, source2$]);
......@@ -23,13 +25,33 @@ result$
console.log(result) // [0,0], [1, 10], [2, 20], [3, 30], ...
})
```
The visual representation of the above example:
## 💡 Gotcha(s)!
Be careful, `combineLatest` will emit it's first value after **all** sources emitted at least one value or completed.
```Typescript
import { interval, combineLatest, NEVER } from 'rxjs';
import { map } from 'rxjs/operators';
const source1$ = NEVER; // neither emitting, nor completing observable
const source2$ = interval(1000).pipe(map(i => i * 10)); // ongoing, never completing source
const result$ = combineLatest([source1$, source2$]);
result$
.subscribe((result) => {
console.log(result) // will never emit any value
})
```
![combineLatest - inner ongoing](assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-combineLatest-inner-ongoing_michael-hladky.png)
A nice behaviour here is even if one of the included Observables completes we process further values from other ongoing Observables.
Even if some sources `complete`, we are able to process future values from other ongoing Observables while keeping the
last emission of the completed ones.
![combineLatest - inner complete](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-combineLatest-inner-complete2_michael-hladky.png)
Only if all included Observables complete the resulting Observable also complete.
`combineLatest` completes when all sources `complete`.
![combineLatest - inner complete all](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-combineLatest-inner-complete_michael-hladky.png)
......
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