Cache Angular HTTP request using @ngneat/cashew

Zaki Mohammed Zaki Mohammed
May 23, 2023 | 4 min read | 968 Views | Comments

The time of not wasting time on caching HTTP request data has come. Presenting @ngneat/cashew package to do the needfully; it takes our stress of manually creating service variables to hold up the HTTP requests data again and again. In this article we will explore @ngneat/cashew dope package for caching HTTP requests.

In any Angular project we create HTTP services to make API calls GET, POST, etc. to operate on server data. Now, in order to preserve the loaded data, we usually create service variables to hold up the data until user make a reload or login again into the system. For this we have to maintain this variable with our bare hand which kind of repetitive task for each newly created service. Storing the response data like forever is not what we usually get paid for. Here, comes super chill @ngneat/cashew package!

We will head in this direction:

  1. Setup Angular App
  2. Add @ngneat/cashew
  3. Add to HTTP service
  4. Use LocalStorage

Setup Angular App

Let us first do the daily chores of any Angular app and as per the need of the hour we are considering v16 of Angular; just for fun we are using Cirrus UI:

ng new ng-cashew-app

npm i cirrus-ui

Below shows the skeleton of the project:

ng-cashew-app
|-- src
|   |-- app
|   |   |-- core
|   |   |   |-- components
|   |   |   |   |-- footer
|   |   |   |   |-- header
|   |   |   |-- pages
|   |   |   |   |-- home
|   |   |   |-- services
|   |   |       |-- core.service.ts
|   |   |-- modules
|   |   |   |-- posts
|   |   |   |-- users
|   |   |-- app-routing.module.ts
|   |   |-- app.component.html
|   |   |-- app.component.ts
|   |   |-- app.module.ts
|-- angular.json
|-- package.json

This application respect minimalism and have less number of routes and components. For the sake of understanding how compression affects the lazy loaded modules we have added 2 feature standalone modules Posts and Users. Thanks to Angular v16 these are standalone components routed in a lazy loaded fashion. The data is coming from our beloved JSON Placeholder API.

Below shows the business logic for Posts component, the Users component is also created in the similar manner:

posts.component.html

<div class="section">
  <div class="content">
    <div class="row mb-3" *ngFor="let post of posts$ | async">
      <div class="col-sm-4">
        <img
          class="img-stretch u-round-md"
          [src]="getImage(post)"
          alt="img-stretch"
          height="400"
          (error)="getDefaultImage($event)" />
      </div>
      <div class="col-lg-6 col-md-8">
        <h3>{{ post.title | titlecase }}</h3>
        <p>{{ post.body }}</p>
        <div>
          <span class="icon">
            <i class="far fa-wrapper fa-clock" aria-hidden="true"></i>
          </span>
          {{randomDate | date: 'fullDate'}}
        </div>
      </div>
    </div>
  </div>
</div>

posts.component.ts

posts$?: Observable;

constructor(private coreService: CoreService) {
  this.posts$ = coreService.getPosts();
}

core.service.ts

private url: string = environment.apiUrl;
private postUrl: string = 'posts';
private userUrl: string = 'users';

constructor(private http: HttpClient) {}

getPosts() {
  const url = `${this.url}${this.postUrl}?userId=1`;
  return this.http.get(url);
}

getUsers() {
  const url = `${this.url}${this.userUrl}`;
  return this.http.get(url);
}

app-routing.module.ts

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'posts', loadComponent: () => import('./modules/posts/posts.component').then(x => x.PostsComponent) },
  { path: 'users', loadComponent: () => import('./modules/users/users.component').then(x => x.UsersComponent) },
  { path: '**', component: HomeComponent },
];

Done with the chores!

Add @ngneat/cashew

Now, let us install @ngneat/cashew package:

npm i @ngneat/cashew

Adding cashew to app module:

app.module.ts

...
import { HttpCacheInterceptorModule } from '@ngneat/cashew';

@NgModule({
  declarations: [...],
  imports: [
    ...
    HttpCacheInterceptorModule.forRoot(), 
  ],
  providers: [],
  bootstrap: [...],
})
export class AppModule {}

Add to HTTP service

Time to add the cashew config to the HTTP options:

core.service.ts

import { withCache } from '@ngneat/cashew';
...

private options = {
  context: withCache()
};

getPosts() {
  const url = `${this.url}${this.postUrl}?userId=1`;
  return this.http.get(url, this.options);
}

getUsers() {
  const url = `${this.url}${this.userUrl}`;
  return this.http.get(url, this.options);
}

Here, the options object holds the context property which is coming from the withCache() method of cashew package. We have added this object as second parameter in the HTTP's get method for post and users endpoints.

Just simply adding withCache() method gives your service methods wings of cache. If you check the network tab you will only find a single call being made to the endpoint, and if you revisit the pages after navigating to some other route, you won't find any calls to the server.

Use LocalStorage

Another question your senior dev in your team will ask is, what about if I refresh the page, will it persist the data in cache? You simply need to assure them that, we need to configure @ngneat/cashew to use localStorage instead of run time memory. This is as simple as eating an omelet (omelet reference!).

We need to add "useHttpCacheLocalStorage" to the providers of app module:

app.module.ts

...
import { HttpCacheInterceptorModule, useHttpCacheLocalStorage } from '@ngneat/cashew';

@NgModule({
  declarations: [...],
  imports: [...],
  providers: [useHttpCacheLocalStorage],
  bootstrap: [...],
})
export class AppModule {}

Last thing is adding version and key to the "withCache" method's object:

core.service.ts

...

private options = {
  context: withCache({
      version: 'v1',
      key: 'omelet'
    })
};

...

Done! Thats it, now we can see the localStorage for this change. Just go to either "posts" or "users" route and check the "Application" tab of developer too and go to "Local Storage". You will see something like this in your local storage.

@ngneat/cashew Local Storage | CodeOmelet

Additionally, we can set the "ttl" (in milliseconds) for this, by default it is for 1 hour. The version is a trick to start using a new local storage as a cache to disband the old cache.

private options = {
  context: withCache({
      version: 'v1',
      key: 'omelet',
      ttl: 3000
    })
};

Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.