The original link: blog. Thoughtram. IO/presents / 201…

This article is translated by RxJS Chinese community, if you need to reprint, please indicate the source, thank you for your cooperation!

If you would like to translate more quality RxJS articles with us, please click here.

Warm tips: The article is quite long, the original is written in 40 minutes to read, suggest you have a lot of free time in the afternoon and then slowly read

Performance is always a priority when developing Web applications. To speed up Angular applications, we can do things like tree-shaking, AHEAD-of-time, lazy loading of modules, and caching. For a comprehensive overview of how to improve performance in Angular applications, we highly recommend you check out the Angular Performance Checklist by Minko Gechev. In this article, we will focus on caching.

In fact, caching is one of the most effective ways to improve a site’s user experience, especially when users are using devices with limited broadband or poor network environments.

There are many ways to cache data or resources. Static resources are usually cached by standard browser caches or Service Workers. While Service Workers can also cache API requests, they are often more useful for caching resources such as images, HTML, JS, or CSS files. We typically use custom mechanisms to cache application data.

Regardless of the mechanism we use, caching generally improves application responsiveness, reduces network overhead, and has the advantage of making content available in the event of a network outage. In other words, when the content is cached closer to the consumer, such as on the client side, the request will not cause additional network activity and the cached data can be retrieved more quickly, saving the entire network round-trip process.

In this article, we will use tools provided by RxJS and Angular to develop an advanced caching mechanism.

directory

  • motivation
  • demand
  • Implementing basic caching
  • Automatic updates
  • Send update notifications
  • Pull new data on demand
  • Looking forward to
  • Special thanks to

motivation

From time to time, people ask how to cache data in Angular applications that use Observables heavily. Most people have a good understanding of how to use Promises to cache data, but when switching to responsive programming they are overwhelmed by its complexity (huge apis), mind-shifting (from imperative to declarative), and many concepts. Therefore, it is difficult to convert an existing cache mechanism based on Promises to an Observables one, especially when you want to make the cache mechanism more advanced.

Angular applications typically use HttpClient in HttpClientModule to perform HTTP requests. All of HttpClient’s apis are Observables, which means that methods like GET, POST, PUT, or Delete return an Observable. Because Observables are inherently lazy, we only initiate the request when we call SUBSCRIBE. However, calling subscribe multiple times to the same Observable causes the source Observable to be recreated over and over again, executing one request per subscription. We call it the cold Observables.

In case you have no idea, we’ve written about this topic before: Cold vs Hot Observables. (For cold vs hot Observables, read this article.)

This behavior makes it tricky to implement the caching mechanism using Observables. Simple approaches tend to require a fair amount of boilerplate code, and we might choose to bypass RxJS, which is feasible, but not recommended if we want to finally harness the power of Observables. Basically, we don’t want to drive a Ferrari with a scooter engine, right?

demand

Before we dive into the code, let’s lay out the requirements for the advanced caching mechanism we want to implement.

The app we want to develop is called Joke World. It’s a simple app that just randomly displays jokes based on a defined category. To make the app simpler and more focused, we just set up one category.

The app has three components: AppComponent, DashboardComponent, and JokeListComponent.

The AppComponent is the entry point to the application. It renders the toolbar and a

, which populates the content based on the current router state.

The DashboardComponent displays only the list of categories. Here you can navigate to the JokeListComponent, which renders the list of jokes to the screen.

Jokes are pulled from the server using Angular’s HttpClient service. To keep the responsibility of the component single and the concept separate, we want to create a JokeService to be responsible for requesting data. The component can then access the data through its public API simply by injecting the service.

This is the architecture of our application, so far there is no caching involved.

When navigating from the category list page to the joke list page, we prefer to request the latest data in the cache rather than making a request to the server every time. The underlying data in the cache is automatically updated every 10 seconds.

Of course, polling for new data every 10 seconds is not a good option for production-grade applications, which typically use a more sophisticated way to update the cache (such as Web Socket push updates). But we’ll keep it simple here so we can focus on the cache itself.

We will receive updates in one form or another. For this application, we don’t want the data in the UI (JokeListComponent) to update automatically when caching updates. Instead, we wait for the user to update the UI. Why do you do that? Imagine a user reading a joke, and then suddenly the joke disappears because of automatic updates. The result is a poor user experience that makes users angry. Therefore, what we do is prompt the user to update whenever there is new data.

To make it more fun, we also want users to be able to force cache updates. This is different from just updating the UI, because forcing updates means requesting the latest data from the server, updating the cache, and updating the UI accordingly.

To summarize the content points we will be developing:

  • The application has two components, A and B, and should request B’s data from the cache when navigating from A to B, not the server each time
  • The cache is automatically updated every 10 seconds
  • The data in the UI is not updated automatically, but needs to be updated by the user
  • Users can force updates, which will make HTTP requests to update the cache and UI

Here’s a preview of the app:

Implementing basic caching

We start simple and work our way up to a mature solution.

The first step is to create a new service.

Next, let’s add two interfaces, one to describe Joke’s data structure and another to enforce the type of HTTP request response. This makes TypeScript happy, but most importantly it makes it easier and clearer for developers to use.

export interface Joke {
  id: number;
  joke: string;
  categories: Array<string>;
}

export interface JokeResponse {
  type: string;
  value: Array<Joke>;
}
Copy the code

Now let’s implement JokeService. We don’t want to expose implementation details as to whether the data comes from the cache or the server, so we’ll just expose the Jokes attribute, which returns an Observable containing a list of jokes.

In order to initiate an HTTP request, we need to ensure that the HttpClien service is injected into the constructor of the service.

Here is the JokeService framework:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable(a)export class JokeService {

  constructor(private http: HttpClient) {}getjokes() { ... }}Copy the code

Next, we’ll implement a private method, requestJokes(), which uses HttpClient to make a GET request to GET the list of jokes.

import { map } from 'rxjs/operators';

@Injectable(a)export class JokeService {

  constructor(private http: HttpClient) {}get jokes() {
    ...
  }

  private requestJokes() {
    return this.http.get<JokeResponse>(API_ENDPOINT).pipe(
      map(response= >response.value) ); }}Copy the code

With that done, we’re left with the Getter for Jokes.

An easy way to do this is to just return this.requestJokes(), but that won’t work. From the beginning of this article we learned that HttpClient exposes all the methods, such as get returning cold Observables. This means that the entire data flow is re-issued for each subscriber, resulting in multiple HTTP requests. After all, the idea of caching is to speed up application loading and minimize the number of network requests.

Instead, we want the flow to be hot. Not only that, but we want every new subscriber to receive the latest cached data. There is a very handy operator called shareReplay. The Observable it returns shares a single subscription to the underlying data source, in this case the one returned by this.requestJokes().

In addition, shareReplay also accepts an optional parameter, bufferSize, which is quite handy for our case. BufferSize determines the maximum number of elements in the replay buffer, that is, the number of elements cached and replayed for each new subscriber. For our scenario, we only want to replay the latest one, so bufferSize will be set to 1.

Let’s take a look at the code and use what we’ve just learned:

import { Observable } from 'rxjs/Observable';
import { shareReplay, map } from 'rxjs/operators';

const API_ENDPOINT = 'https://api.icndb.com/jokes/random/5?limitTo=[nerdy]';
const CACHE_SIZE = 1;

@Injectable(a)export class JokeService {
  private cache$: Observable<Array<Joke>>;

  constructor(private http: HttpClient) {}get jokes() {
    if (!this.cache$) {
      this.cache$ = this.requestJokes().pipe(
        shareReplay(CACHE_SIZE)
      );
    }

    return this.cache$;
  }

  private requestJokes() {
    return this.http.get<JokeResponse>(API_ENDPOINT).pipe(
      map(response= >response.value) ); }}Copy the code

Ok, we’ve already covered most of the above code. But wait, what does the if statement in the private cache$and getter methods do? The answer is simple. If you return this.requestjokes ().pipe(shareReplay(CACHE_SIZE)) directly, a cache instance will be created for each subscription. But what we want is for all subscribers to share the same instance. Therefore, we store this shared instance in the private property cache$and initialize it the first time the getter method is called. All subsequent consumers will share this instance without having to recreate the cache each time.

To visualize what we just implemented, use the following figure:

In the figure above, we can see a sequence diagram describing the objects involved in our scenario, namely the request joke list and the queue exchanging messages between the objects. Let’s break it down to get a better idea of what we’re doing.

Let’s start by navigating the DashboardComponent to the JokeListComponent.

After component initialization, Angular calls the ngOnInit lifecycle hook, where we call the Getter method for Jokes exposed by JokeService to request a list of jokes. Because this is the first time the data is requested, the cache itself is not initialized, that is, JokeService. Cache $is undefined. Internally we call requestJokes(), which returns an Observable that will emit server-side data. We also apply the shareReplay operator to get the desired effect.

The shareReplay operator automatically creates a ReplaySubject between the original data source and all subsequent subscribers. Once the number of subscribers increases from 0 to 1, the Subject connects to the underlying source Observable and broadcasts all its replay values. All subsequent subscribers connect to the man-in-the-middle Subject, so the underlying cold Observable has only one subscription. This is called multicast, and it is the basis of our simple caching mechanism. (For more on multicast, read this article.)

Once the server returns the data, it is cached.

Note that the Cache is a separate object in the sequence diagram, represented as a ReplaySubject between the consumer (subscriber) and the underlying data source (HTTP request).

When data is requested again for the JokeListComponent, the cache will replay the latest value and send it to the consumer. No additional HTTP requests can be made.

Simple, right?

To see more details, we need to take a closer look at how caching works at the Observable level. Therefore, we will use the marble diagram to visually demonstrate how convection works:

The pinball map looks pretty clear. The underlying Observable really only has one subscription, and all consumers subscribe to the same shared Observable (ReplaySubject). We can also see that only the first subscriber triggered the HTTP request, while the other subscribers only got the latest value of the cache replay.

Finally, let’s look at the JokeListComponent and how to present the data. The first step is to inject JokeService. The Jokes $property is then initialized during the ngOnInit life cycle as an Observable returned by the getter method exposed by the service, which is of type Array

, which is exactly what we want.

@Component({... })export class JokeListComponent implements OnInit {
  jokes$: Observable<Array<Joke>>;

  constructor(private jokeService: JokeService) { }

  ngOnInit() {
    this.jokes$ = this.jokeService.jokes; }... }Copy the code

Note that we don’t subscribe compulsively to the script $, but use the Async pipe in the template because it’s so endearingly easy to use. Curious? Check out this article: Three Things you need to know about AsyncPipe

<mat-card *ngFor="let joke of jokes$ | async">.</mat-card>
Copy the code

Cool! So that’s our simple cache. To verify that a request is made only once, open Chrome’s Developer tools, click on the Network TAB, and select XHR. Start from the category list page, navigate to the joke list page, and then return to the category list page, over and over again.

Stage 1 Online Demo: Click to view.

Automatic updates

So far, we’ve developed a simple caching mechanism with a little code, and most of the dirty work is done by the shareReplay operator, which caches and replays the latest values.

It works fine, but the data source in the background is never updated. What if the data might change every few minutes? We don’t want to force users to refresh the entire page to get the latest data from the server.

Wouldn’t it be great if our cache could be updated in the background every 10 seconds? Couldn’t agree more! As users, we don’t have to reload the page, and if the data changes, the UI updates accordingly. Again, in a real application we would almost never use polling, but server push notifications. But for our small Demo application, a 10-second interval timer is sufficient.

It’s also fairly simple to implement. In short, we want to create an Observable that emits a series of values separated by a given interval, or to put it more simply, we want to generate a value every x milliseconds. We have several ways of doing this.

The first option is to use interval. This operator accepts an optional parameter, period, which defines the time interval between each emitted value. See the following example:

import { interval } from 'rxjs/observable/interval';

interval(10000).subscribe(console.log);
Copy the code

The Observable we set up emits an infinite sequence of integers, 10 seconds apart. That means the first value will be emitted in 10 seconds. To better illustrate, let’s look at the pinball diagram of the interval operator:

Well, that’s true. The first value is emitted “delayed,” which is not what we want. Why do you say so? Because if we navigate from the category list page to the joke list page, we have to wait 10 seconds before making a data request to the server to render the page.

We can fix this by introducing another operator called startWith(value) so that the given value, the initial value, is emitted first. But we can do better!

If I told you that there was another operator, it would emit values at a given time (initial delay) and then continuously emit values at a time interval (regular timer). Timer takes a look.

Pinball time!

Cool, but does it really solve our problems? Yes, that’s right. If we set the initial delay to 0 and the interval to 10 seconds, it behaves the same as interval(10000).pipe(startWith(0)), but uses only one operator.

Let’s use the timer operator and apply it to our existing caching mechanism.

We need to set a timer and make an HTTP request to pull the latest data from the server every time the time is up. In other words, at each point in time we need to switch to an Observable that gets a list of jokes using switchMap. A nice side effect of using swtichMap is to avoid conditional contention. This is due to the nature of the operator, which unsubscribes to the previous internal Observable and emits only the values from the latest internal Observable.

The rest of our cache remains intact, our stream is still multicast, and all subscribers share the same underlying data source.

Similarly, shareReplay sends the latest value to existing subscribers and replays the latest value for subsequent subscribers.

As shown in the pinball diagram, the timer emits a value every 10 seconds. Each value is converted into an internal Observable that pulls the data. By using the switchMap, we can avoid the race condition, so the consumer will only receive the values 1 and 3. The value of the second internal Observable gets “skipped” because we’ve already unsubscribed to it when the new value is emitted.

Let’s apply what we’ve just learned to JokeService:

import { timer } from 'rxjs/observable/timer';
import { switchMap, shareReplay } from 'rxjs/operators';

const REFRESH_INTERVAL = 10000;

@Injectable(a)export class JokeService {
  private cache$: Observable<Array<Joke>>;

  constructor(private http: HttpClient) {}get jokes() {
    if (!this.cache$) {
      // Set the timer to emit values every X milliseconds
      const timer$ = timer(0, REFRESH_INTERVAL);

      // HTTP requests are made at each point in time to get the latest data
      this.cache$ = timer$.pipe(
        switchMap(_ => this.requestJokes()),
        shareReplay(CACHE_SIZE)
      );
    }

    return this.cache$; }... }Copy the code

Cool! Would you like to try it yourself? Try the following online Demo regularly. Navigate from the category list page to the joke list page, and witness the miracle. Wait patiently for a few seconds before you see the data update. Remember that while the cache is refreshed every 10 seconds, you are free to change the value of REFRESH_INTERVAL in the online Demo.

Stage 2 Online Demo: Click to view.

Send update notifications

Let’s briefly review what we’ve developed so far.

When requesting data from JokeService, we always want to request the latest data in the cache, not the server every time. The cached underlying data is refreshed every 10 seconds and propagated to the component causes the UI to update automatically.

It was a bit of a failure. Imagine that we’re the user and we’re looking at a joke and all of a sudden the joke disappears because the UI updates automatically. This kind of poor user experience can make users angry.

Therefore, users should be notified when new data is available. In other words, we want users to perform UI updates.

In fact, we don’t even need to modify the service layer to do this. The logic is fairly simple. After all, our service layer shouldn’t care about sending notifications and when and how to update the data on the screen; it should be the view layer that takes care of that.

First, we need to present the initial value to the user, otherwise the screen will be blank before the first cache update. We’ll see why in a minute. Setting the stream of initial values is as simple as calling the getter method. Also, since we are only interested in the first value, we can use the take operator.

To make the logic reusable, we create a helper method getDataOnce().

import { take } from 'rxjs/operators';

@Component({... })export class JokeListComponent implements OnInit {
  ...
  ngOnInit() {
    const initialJokes$ = this.getDataOnce(); . } getDataOnce() {return this.jokeService.jokes.pipe(take(1)); }... }Copy the code

As required, we only want to update the UI when the user actually performs the update, not automatically. So how do users implement the updates you require? We do this when we click the “Update” button in the UI. For now, let’s forget about notifications and focus on the update logic when the button is clicked.

To do this, we need a way to create observables that derive from DOM events, in this case button clicks. There are several ways to create this, but the most common is to use Subject as a bridge between the template and the logic in the component class. In short, Subject is a type that implements both an Observer and an Observable. Observables define data flows and generate data, and observers can subscribe to Observables and receive data.

The nice thing about Subject is that we can use event binding directly in the template and then call the next method when the event fires. This broadcasts the specific value to all observers who are listening for the value. Note that we can also omit this value if Subject is of type void. In fact, this is our actual scenario.

Let’s instantiate a new Subject.

import { Subject } from 'rxjs/Subject';

@Component({... })export class JokeListComponent implements OnInit {
  update$ = new Subject<void> (); . }Copy the code

Then we can use it in the template.

<div class="notification">
  <span>There's new data available. Click to reload the data.</span>
  <button mat-raised-button color="accent" (click) ="update$.next()">
    <div class="flex-row">
      <mat-icon>cached</mat-icon>
      UPDATE
    </div>
  </button>
</div>
Copy the code

How do we use event binding syntax to capture click events on

Another way is to use the @ViewChild() decorator and RxJS ‘fromEvent operator. However, this requires us to “mix” the DOM in the component class and query the HTML elements from the view. With Subject, we just bridge the two and don’t touch the DOM at all, except for the event bindings we add to the button.

Ok, with our view set up, we can switch to the logic that handles UI updates.

So what does updating the UI mean? The cache is updated automatically in the background, and we only render the latest value from the cache when we want to click the button, right? This means that our source stream is Subject. Each time a value is emitted on update$, we map it to an Observable that gives the latest cached value. In other words, we use a Higher Order Observable, that is, an Observable that generates Observables.

Until then, we should know that switchMap solves just this problem. But this time, we’ll use mergeMap. It behaves much like switchMap in that it does not unsubscribe from the previous internal Observable, but instead merges the emitted values of the internal Observable into the output Observable.

In fact, by the time the latest value is requested from the cache, the HTTP request has already completed and the cache has been successfully updated. Therefore, we do not face the problem of conditional competition. Although this looks asynchronous, it is synchronous to some extent because the values are emitted in the same tick.

import { Subject } from 'rxjs/Subject';
import { mergeMap } from 'rxjs/operators';

@Component({... })export class JokeListComponent implements OnInit {
  update$ = new Subject<void> (); . ngOnInit() { ...const updates$ = this.update$.pipe(
      mergeMap((a)= > this.getDataOnce()) ); . }... }Copy the code

Cool! Each “update” we request the latest value from the cache, which uses the helper method we implemented earlier.

At this point, we are one small step away from completing the flow responsible for rendering jokes to the screen. All we need to do is merge the initialJokes$and update$streams.

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { mergeMap } from 'rxjs/operators';

@Component({... })export class JokeListComponent implements OnInit {
  jokes$: Observable<Array<Joke>>;
  update$ = new Subject<void> (); . ngOnInit() {const initialJokes$ = this.getDataOnce();

    const updates$ = this.update$.pipe(
      mergeMap((a)= > this.getDataOnce())
    );

    this.jokes$ = merge(initialJokes$, updates$); . }... }Copy the code

It is important that we use the helper method getDataOnce() to map each update event to the latest cached value. Recall that inside this method we use take(1), which just takes the first value and completes the flow. This is critical, otherwise you end up with a stream that is in-progress or connected to the cache in real time. In this case, it basically breaks the logic that we perform UI updates just by clicking the “Update” button.

Also, because the underlying cache is multicast, it is perfectly safe to always re-subscribe to the cache to get the latest values.

Before continuing with the notification flow, let’s pause and look at the pinball diagram that just implemented the logic.

As you can see in the figure, initialJokes$is key because without it we can only see the list of jokes on the screen after clicking the “Update” button. Although the data was updated in the background every 10 seconds, we couldn’t click the update button at all. The button itself is part of the notification, but we never showed it to the user.

So, let’s fill in the blanks and implement the missing functionality.

We need to create an Observable to show/hide notifications. Essentially, we need a stream that emits true or false. We want the value to be true when updating, and false when the user clicks the “update” button.

Also, we want to skip the first (initial) value emitted by the cache because it is not new data.

Using the flow mentality, we can break it up into streams and combine them into a single Observable. The resulting stream will have the behavior required to show or hide notifications.

So much for the theory! Here’s the code:

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { skip, mapTo } from 'rxjs/operators';

@Component({... })export class JokeListComponent implements OnInit {
  showNotification$: Observable<boolean>;
  update$ = new Subject<void> (); . ngOnInit() { ...const initialNotifications$ = this.jokeService.jokes.pipe(skip(1));
    const show$ = initialNotifications$.pipe(mapTo(true));
    const hide$ = this.update$.pipe(mapTo(false));
    this.showNotification$ = merge(show$, hide$); }... }Copy the code

Here, we skip the first value in the cache and listen for all its remaining values because the first value is not new data. We mapped each value emitted by initialNotifications$to true to display notifications. Once we click the “Update” button in the notification, update$produces a value that we can map to false to turn off the notification.

We use showNotification$in the template of the JokeListComponent to toggle the class to show/turn off notifications.

<div class="notification" [class.visible] ="showNotification$ | async">.</div>
Copy the code

Yeah! At the moment, we are very close to the final solution. Before we move on, let’s play an online Demo. Take your time and walk through the code one more time.

Stage 3 Online Demo: Click to view.

Pull new data on demand

Cool! We’ve implemented some cool features for our cache along the way. There is one more thing we need to do to end this article and take caching up another level. As users, we want to be able to force updates at any point in time.

There is nothing complicated about this, but to accomplish this we need to modify both the component and the service.

Let’s start with service. We need a public-facing API to enforce caching of overloaded data. Technically, we complete the current cache and set it to NULL. This means that the next time we request data from the service, we set up a new cache that pulls the data from the server and stores it for later subscribers. Creating a new cache every time an update is forced is not a big deal because the old cache will be completed and eventually garbage collected. In fact, this has the useful side effect of resetting the timer, which is exactly what we want. For example, we wait 9 seconds and then click the “Force Update” button. We expect the data to refresh, but we don’t want to see an update notification pop up one second later. We want to restart the timer so that it should be another 10 seconds before automatic updates are triggered when forced.

Another reason to destroy the cache is that it is much less complex than the version that does not destroy the cache. In the latter case, the cache needs to know if reloading data is mandatory.

Let’s create a Subject that notifies the cache to complete. Here we take advantage of the takeUnitl operator and add it to the cache$stream. In addition, we implemented a public API that uses Subject to broadcast events while setting the cache to NULL.

import { Subject } from 'rxjs/Subject';
import { timer } from 'rxjs/observable/timer';
import { switchMap, shareReplay, map, takeUntil } from 'rxjs/operators';

const REFRESH_INTERVAL = 10000;

@Injectable(a)export class JokeService {
  private reload$ = new Subject<void> (); .get jokes() {
    if (!this.cache$) {
      const timer$ = timer(0, REFRESH_INTERVAL);

      this.cache$ = timer$.pipe(
        switchMap((a)= > this.requestJokes()),
        takeUntil(this.reload$),
        shareReplay(CACHE_SIZE)
      );
    }

    return this.cache$;
  }

  forceReload() {
    // Call 'next' to complete the current cache stream
    this.reload$.next();

    // Set the cache to 'null' so that the next time 'jokes' are called
    // a new cache is created
    this.cache$ = null; }... }Copy the code

Light is not implemented in the service; we need to use it in the JokeListComponent. To do this, we will implement a function, forceReload(), which is called when the force Update button is clicked. In addition, we need to create a Subject as an Event Bus for updating the UI and displaying notifications. We’ll see what it does in a minute.

import { Subject } from 'rxjs/Subject';

@Component({... })export class JokeListComponent implements OnInit {
  forceReload$ = new Subject<void> (); . forceReload() {this.jokeService.forceReload();
    this.forceReload$.next(); }... }Copy the code

This allows us to associate the buttons in the JokeListComponent template to force the cache to reload the data. All we need to do is use Angular’s event-binding syntax to listen for the click event and call forceReload() when a button is clicked.

<button class="reload-button" (click) ="forceReload()" mat-raised-button color="accent">
  <div class="flex-row">
    <mat-icon>cached</mat-icon>
    FETCH NEW JOKES
  </div>
</button>
Copy the code

This should work, but only if we go back to the categories list page and then back to the jokes list page. This is certainly not what we want. We want to update the UI immediately when caching overloaded data is forced.

Remember the stream update$we already implemented? When we click the update button, it requests the latest data in the cache. In fact, we need the same behavior, so we can continue to use and extend the flow. This means we need to merge update$and forceReload$, since both streams are data sources for UI updates.

import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { mergeMap } from 'rxjs/operators';

@Component({... })export class JokeListComponent implements OnInit {
  update$ = new Subject<void> (); forceReload$ =new Subject<void> (); . ngOnInit() { ...const updates$ = merge(this.update$, this.forceReload$).pipe(
      mergeMap((a)= > this.getDataOnce()) ); . }... }Copy the code

It’s that simple, isn’t it? Yes, but it’s not over yet. In fact, all we do is “break” notifications. Everything worked until we hit the “Force Update” button. Once the button is clicked, the data in the screen and cache is updated as usual, but no notification pops up after waiting 10 seconds. The problem is that forced updates complete the cache stream, which means that the value is no longer received in the component. The notification stream (initialNotifications$) is basically dead. That’s not the right result, so how can we fix it?

Pretty simple! We listen for events emitted by forceReload$, switching each emitted value to a new notification flow. It is important here to unsubscribe from the previous stream. Are there bells ringing in your ears? As if telling us we need to use switchMap here.

Let’s implement the code!

import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { take, switchMap, mergeMap, skip, mapTo } from 'rxjs/operators';

@Component({... })export class JokeListComponent implements OnInit {
  showNotification$: Observable<boolean>;
  update$ = new Subject<void> (); forceReload$ =new Subject<void> (); . ngOnInit() { ...const reload$ = this.forceReload$.pipe(switchMap((a)= > this.getNotifications()));
    const initialNotifications$ = this.getNotifications();
    const show$ = merge(initialNotifications$, reload$).pipe(mapTo(true));
    const hide$ = this.update$.pipe(mapTo(false));
    this.showNotification$ = merge(show$, hide$);
  }

  getNotifications() {
    return this.jokeService.jokes.pipe(skip(1)); }... }Copy the code

That’s all. Every time forceReload$emits a value, we unsubscribe from the previous Observable and switch to a new notification stream. Note that there is a line of code we need to call twice, is this. JokeService. Jokes. The pipe (skip (1)). To avoid duplication, we create the function getNotifications(), which returns a stream of jokes but skips the first value. Finally, we merged initialNotifications$and Reload $into a stream called show$. This stream is responsible for displaying notifications on the screen. There is also no need to unsubscribe from initialNotifications$because it will be done before the cache is recreated. Everything else remains the same.

Well, we did it. Let’s take a moment to look at the pinball diagram we just implemented.

As you can see in the figure, initialNotifications$is important for displaying notifications. Without this stream, we would only get a chance to see notifications after forcing a cache update. That is, when we request the latest data on demand, we must constantly switch to a new notification stream because the previous (old) Observable has finished and no longer emits any values.

That’s it! We implemented a sophisticated caching mechanism using tools provided by RxJS and Angular. Briefly, our service exposed a stream that provided us with a list of jokes. HTTP requests are triggered every 10 seconds to update the cache. To improve the user experience, we have provided update notifications so that users can perform UI updates. On top of that, we provide a way for users to request up-to-date data on demand.

That’s great! This is the complete solution. Take a few minutes to go over the code again. Then try different scenarios to make sure everything works.

Stage 4 Online Demo: Click to view.

Looking forward to

If you want to do some homework or develop your brain power later, here are a few ideas:

  • Add error handling
  • Refactoring logic from components into services to make it reusable

Special thanks to

Special thanks to Kwinten Pisman for helping me write the code. I’d also like to thank Ben Lesh and Brian Troncone for giving me valuable feedback and suggesting some improvements. Also, many thanks to Christoph Burgdorf for his review of the article and code.