{ "community_link": "https://community.indepth.dev/t/fastest-way-to-cache-for-lazy-developers-angular-with-rxjs/500" }

Fastest way to cache for lazy developers — Angular with RxJS

In this article I will explain the fastest way to cache for lazy developers — Angular with RxJS.

Fastest way to cache for lazy developers — Angular with RxJS

HTTP caching simply means the browser stores local copies of web resources for faster retrieval the next time the resource is required, thus reducing the number of server calls.

The aim of this article is to show how you can implement caching with only two RxJS operators: publishReplay() and refCount().

When you search for RxJS caching, you’ll most likely find articles, forums, and discussions that recommend either shareReplay() or publishReplay() with refCount(). In short, shareReplay() is similar to publishReplay() except you need it with refCount() (source A, source B). And for this article, we’ll use publishReplay() with refCount().

If you want to learn more about the difference between these two operators, here’s an in-depth article.

If you want to become a better web developer, start your own business, teach others, or simply improve your development skills, follow to get notified when new articles are published here.

Topics we’ll cover

  1. What is caching?
  2. Caching with RxJS + two operators
  3. A practical example of how to cache
  4. Summary

1. What is caching?

Caching was initially developed to reduce the access time between the CPU and RAM. It is mainly used to store data that will most likely be accessed in the near time (seconds, minutes or hours later). So if you wonder why things go much faster the second time you reload something (refresh a page), that’s when you truly see the power of caching.

Why cache? Cache memory is much faster to access by the CPU than RAM because it’s physically located inside the CPU. It’s like instead of going out to buy a hammer, you just use the one close to you at home. Shorter distance means faster access time.

There are many ways one can implement caching, you can use NgRx to handle states, JavaScripts’s API localStorage, or simply an array that holds the values. Some are easy to implement, while others increase complexity and make it difficult to keep track of what’s happening, for instance when you need to update/reset the cache etc.

2. Caching with RxJS (publishReplay, refCount)

In order to cache an HTTP request, you need to add two awesome operators from the RxJS library: publishReplay() and refCount().

import { from } from 'rxjs'; 
import { map, publishReplay, refCount } from 'rxjs/operators';


// Create observable that holds two values
const observable$ = from(['first', 'last']).pipe(
  publishReplay(1), 
  refCount()
)

// First time subscribing, we get both values
observable$.subscribe(data => console.log(data));

// Second time subscribing, we get the latest value
observable$.subscribe(data => console.log(data));
observable$.subscribe(data => console.log(data));
observable$.subscribe(data => console.log(data));

// OUTPUT
// first
// last
// last
// last
// last
cache observable

As you can see, the first time we subscribe we get two values first and last. Afterwards, we only get the last value despite how many times we subscribe, this concept is where we do caching.

This allows us to reduce the number of server calls. But how? In short, instead of sending a new HTTP request to the server, we do it once. The second, third and fourth time we subscribe it returns the cached value instead of doing a whole new server call.

We need a bit of extra logic to fully utilize caching, but the concept here is what allows us to do it. The next section covers a real world example.

Before we jump into the next section: why do we need publishReplay() with refCount()?

PublishReplay operator

The publishReplay() operator turns a cold observable to a hot observable by multi-casting the values. It creates a new Subject of type ReplaySubject(). The argument we pass determines the current index of the value we want to replay. For example, an array of ['A', 'B', 'C'] with publishReplay(1) means we want to replay the last value which is C. To further elaborate, if we do publishReplay(2), we replay the two last values which is B and C.

refCount operator

refCount() keeps track of all subscribers so that it can unsubscribe from the original source when all the subscribers are gone.

3. A practical example of how to cache

Let’s say we have a dashboard page that sends a request to the server. And for each dashboard page, we fetch a JSON configuration file from the server that provides some meta-info about which components to show/hide on the page. The file rarely changes (on a weekly-basis), and the data for the most part is identical.

So far so good, but what if we have thousands of users with x number of page visits, and for each visit the configuration file is re-fetched from the server. This results in thousands of unnecessary server requests.

This is not a beneficial, cost- and performance wise approach. We need some sort of logic where we can reuse the data instead of running multiple server requests.

Let’s take a look of how we can implement the cache logic with RxJS in our service file. Note, this example is Angular specific, however, you can use the same principle with any other observables that offer these two operators.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, publishReplay, refCount } from 'rxjs/operators';

export interface Config {
    componentType: string, 
    show: Boolean
}

@Injectable({
    providedIn: 'root'
})

export class ConfigService {

    configs: Observable<Config[]>;

    constructor(private httpClient: HttpClient) { }

    // Get configs from server | HTTP GET
    getConfigs(): Observable<Config[]> {

        // Cache it once if configs value is false
        if (!this.configs) {
            this.configs = this.httpClient.get(`${api_url}/configs`).pipe(
                map(data => data['configs']),
                publishReplay(1), // this tells Rx to cache the latest emitted
                refCount() // and this tells Rx to keep the Observable alive as long as there are any Subscribers
            );
        }

        return this.configs;
    }

    // Clear configs
    clearCache() {
        this.configs = null;
    }

}

As shown in the example above, we leave the caching implementation in the service file (which makes sense), and then as we normally do, manually subscribe to access the data, or automatically subscribe using Angular’s awesome async pipe.

3.1. Create a variable reference

This allows us to share the value between other methods within the class. It’s also necessary if we want to clear it.

... 

export class ConfigService {

    configs: Observable<Config[]>;

    ....

}

3.2. The cache logic

The first time we execute the getConfigs() method, it stores (caches) the result. This is achieved by having an if-statement that checks if the value this.configs has been set to true or false. In other words: it is either cached or not cached. If the value of this.configs is false, we send a GET request to the server, and if true, we skip the GET request and use the cached value.

// Get configs from server | HTTP GET
getConfigs(): Observable<Config[]> {

  // Cache it once if configs value is false
  if (!this.configs) {
    this.configs = Observable$.pipe(
      // ... operators
      publishReplay(1),
      refCount()
     );
  }
  return this.configs;
}


3.3. Clearing the cache

If you want to update the cache, you simply set the reference value this.configs to null. Then when the method getConfigs() is executed again, it will run a new server request which then updates the previous stored cache.

// Clear configs
clearCache() {
  this.configs = null;
}

4. Summary

Caching is a viable choice if we want to save numbers of server requests, especially for data that rarely changes such as daily, weekly etc. However, one should always be cautious when dealing with caching and don’t overdo it. The cache memory has a small size of kilobytes/megabytes, and is mostly used for storing things temporary.

Want to learn more about Subjects and Observables, check out these series about reactive programming to boost your reactive skills.
An introduction to observables in Reactive Programming
One of the most challenging things for new developers to learn is the observerpattern. Understanding how to effectively use it with RxJS to deal withasynchronous data such as user events, HTTP requests or any other events thatrequire waiting for something to complete is tricky. What most people …
An introduction to Subjects in Reactive Programming
A Subject is a “special” type of observable that allows us to broadcast valuesto multiple subscribers. The cool thing about Subjects, is that it provides areal-time response. For instance, if we have a subject with 10 subscribers, whenever we push valuesto the subject, we can see the value captu…