Commit cd158540 authored by Julian's avatar Julian
Browse files

improve combineLatest exercises

parent 87680965
# Combining ongoing Observables - Exercise # 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_over-fetching_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_http-caching_michael-hladky.png)
## Intro This is a result of wrong state-management. To face this problem, we need to store the results of the HTTP Requests in
As processing HTTP calls directly in the component we run into "over-fetching" of data. a shared cache. We can put this behavior inside a `Service` in order to share our state accross multiple `Components`.
Over-fetching means we request data from the server more often than we need to.
This is a result of wrong state-management. _Shared data store visualized_
We need the result of the HTTP request on a global level in the service. ![](./assets/images/Reactive-architecture-and-ux-patterns_angular_http-caching_michael-hladky.png)
From there, HTTP result can be cached, share with multiple components and replayed from the cache if needed.
For this exercise we introduce a very basic HTTP cache solution by using a `BehaviorSubject` as a shared data store. Instead
To do so, we implemented a BehaviorSubject in our service as well as of binding `Components` directly to HTTP Requests, we will feed the data store with the results and provide
some new methods to update the services state. single shared `Observables` mapped from the data store.
**Service** **Service**
```Typescript ```Typescript
...@@ -23,37 +26,40 @@ private readonly state$ = new BehaviorSubject<BlogServiceState>({ ...@@ -23,37 +26,40 @@ private readonly state$ = new BehaviorSubject<BlogServiceState>({
comments: [] as Comment[] comments: [] as Comment[]
}); });
readonly posts$ = this.state$.pipe(map(s => s.posts)); readonly posts$ = this.state$.pipe(map(s => s.posts)); // shared and replayed observable posts
readonly comments$ = this.state$.pipe(map(s => s.comments)); readonly comments$ = this.state$.pipe(map(s => s.comments)); // shared and replayed observable comments
// ... // ...
fetchPosts() { fetchPosts() {
this.httpGetPosts() this.httpGetPosts()
.subscribe(posts => { .subscribe(posts => {
this.state$.next({ this.state$.next({
...this.state$.getValue(), ...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, posts, 'id') posts: upsertEntities(this.state$.getValue().posts, posts, 'id')
}); });
}); });
} }
fetchComments() { fetchComments() {
this.httpGetComments() this.httpGetComments()
.subscribe(comments => { .subscribe(comments => {
this.state$.next({ this.state$.next({
...this.state$.getValue(), ...this.state$.getValue(),
comments: upsertEntities(this.state$.getValue().comments, comments, 'id') comments: upsertEntities(this.state$.getValue().comments, comments, 'id')
}); });
}); });
} }
addPost(post: Pick<Post, 'title'>) { addPost(post: Pick<Post, 'title'>) {
this.httpPostPost(post) this.httpPostPost(post)
.subscribe((v) => { .subscribe((newPost) => {
console.log(v); console.log('saved ', newPost , 'to the server');
// this.fetchPosts(); this.state$.next({
}, console.log); ...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, [newPost], 'id')
})
}, console.log);
} }
``` ```
...@@ -72,17 +78,19 @@ constructor(...) { ...@@ -72,17 +78,19 @@ constructor(...) {
## Exercise ## Exercise
Use the properties `posts$` and `comments$` instead of `httpGetPosts` and `httpGetComments`. 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
If you click the "add post" button you will notice the calculated blog posts don't emit anymore. `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. `combineLatest` is perfectly suited for this case.
In this case `combineLatest` is a perfect match.
Use it and see if the list of BlogPosts renders now. 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 ongoing Observables - Solution
Combining the http calls with the `combineLatest` operator: Combining the http requests with the `combineLatest` operator:
**Component** **Component**
```Typescript ```Typescript
...@@ -15,4 +15,5 @@ blog$: Observable<BlogPost[]> = combineLatest([ ...@@ -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 # `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. 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 Both combine the values of the source Observables to one emission, but `combineLatest` in comparison to `forkJoin` does not rely
process **every new value** with the latest values of the other included Observables. 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) ![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 ```Typescript
import { interval, combineLatest } from "rxjs"; import { interval, combineLatest } from 'rxjs';
import { map } from "rxjs/operators"; 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 source2$ = interval(1000).pipe(map(i => i * 10));
const result$ = combineLatest([source1$, source2$]); const result$ = combineLatest([source1$, source2$]);
...@@ -23,13 +25,33 @@ result$ ...@@ -23,13 +25,33 @@ result$
console.log(result) // [0,0], [1, 10], [2, 20], [3, 30], ... 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) ![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) ![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) ![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