DEV Community

Mauro Garcia
Mauro Garcia

Posted on Edited on Originally published at maurogarcia.dev

Client-side caching with Angular

The time it takes our applications to show useful information for our users has a great impact on the user experience. That's why I think it's our responsibility as software developers to implement mechanisms that allow us to reduce this loading time as much as possible.

In this article, I'm gonna show you how to implement client-side caching with Angular.
By the end of this post, you'll be able to cache your http request like this:

return this._http.get<Product[]>({ url: 'https://example-api/products', cacheMins: 5 })
ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

For this implementation, we'll need:

  • A cache service: This service will be required for two main things:
    • Save data in the localstorage (with expiration)
    • Load data from the localstorage.
  • A custom http-client service: This service will use the angular HttpClient under the hood, but will also use the cache service mentioned above to get and save data from/to localstorage.

cache.service.ts

import { Injectable } from '@angular/core'

@Injectable()
export class CacheService {
    constructor() { }

    save(options: LocalStorageSaveOptions) {
        // Set default values for optionals
        options.expirationMins = options.expirationMins || 0

        // Set expiration date in miliseconds
        const expirationMS = options.expirationMins !== 0 ? options.expirationMins * 60 * 1000 : 0

        const record = {
            value: typeof options.data === 'string' ? options.data : JSON.stringify(options.data),
            expiration: expirationMS !== 0 ? new Date().getTime() + expirationMS : null,
            hasExpiration: expirationMS !== 0 ? true : false
        }
        localStorage.setItem(options.key, JSON.stringify(record))
    }

    load(key: string) {
        // Get cached data from localstorage
        const item = localStorage.getItem(key)
        if (item !== null) {
            const record = JSON.parse(item)
            const now = new Date().getTime()
            // Expired data will return null
            if (!record || (record.hasExpiration && record.expiration <= now)) {
                return null
            } else {
                return JSON.parse(record.value)
            }
        }
        return null
    }

    remove(key: string) {
        localStorage.removeItem(key)
    }

    cleanLocalStorage() {
        localStorage.clear()
    }
}

export class LocalStorageSaveOptions {
    key: string
    data: any
    expirationMins?: number
}
ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

http-client.service.ts

import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { CacheService } from './cache.service'
import { Observable, of } from 'rxjs'
import { switchMap } from 'rxjs/operators'

export enum Verbs {
    GET = 'GET',
    PUT = 'PUT',
    POST = 'POST',
    DELETE = 'DELETE'
}

@Injectable()
export class HttpClientService {

    constructor(
        private http: HttpClient,
        private _cacheService: CacheService,
    ) { }

    get<T>(options: HttpOptions): Observable<T> {
        return this.httpCall(Verbs.GET, options)
    }

    delete<T>(options: HttpOptions): Observable<T> {
        return this.httpCall(Verbs.DELETE, options)
    }

    post<T>(options: HttpOptions): Observable<T> {
        return this.httpCall(Verbs.POST, options)
    }

    put<T>(options: HttpOptions): Observable<T> {
        return this.httpCall(Verbs.PUT, options)
    }

    private httpCall<T>(verb: Verbs, options: HttpOptions): Observable<T> {

        // Setup default values
        options.body = options.body || null
        options.cacheMins = options.cacheMins || 0

        if (options.cacheMins > 0) {
            // Get data from cache
            const data = this._cacheService.load(options.url)
            // Return data from cache
            if (data !== null) {
                return of<T>(data)
            }
        }

        return this.http.request<T>(verb, options.url, {
            body: options.body
        })
            .pipe(
                switchMap(response => {
                    if (options.cacheMins > 0) {
                        // Data will be cached
                        this._cacheService.save({
                            key: options.url,
                            data: response,
                            expirationMins: options.cacheMins
                        })
                    }
                    return of<T>(response)
                })
            )
    }
}

export class HttpOptions {
    url: string
    body?: any
    cacheMins?: number
}
ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

Now, let's say we have a product service we use to retrieve a list of products from our API. In this service we'll use our recently created http-client service to make a request and save the data in the localstorage for 5 minutes:

// product.service.ts

import { Injectable } from '@angular/core'
import { HttpClientService } from './http-client.service'
import { Observable } from 'rxjs'

@Injectable()
export class ProductService {

    constructor(
        private _http: HttpClientService
    ) { }

    getAll(): Observable<Product[]> {
        return this._http
            .get<Product[]>({ url: 'https://example-api/products', cacheMins: 5 })
    }
}

export class Product {
    name: string
    description: string
    price: number
    available: boolean
}
ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”

What do you think about this strategy? Are you using other techniques like http-interceptor? Let me know in the comments below

Top comments (14)

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
kdfemi profile image
MiniSoda •

Correct me i i am wrong your cached data is served but no request is made to check if there’s any update to the cached version

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
mauro_codes profile image
Mauro Garcia •

If you want to ignore the cached version, you just need to call the remove method of the cache service. Then, the http-client will return null after trying to get the data from cache and will fetch new data from the server

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
kdfemi profile image
MiniSoda •

I don’t mean ignoring the cached version, i feel the user should be served the cached version first the a request to the server should also be sent instead of the cached version only served without making any request to the serve. I don’t know if it an efficient approach thou

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” Thread
 
mauro_codes profile image
Mauro Garcia •

I think that what you say may be or may not be the best solution based on your requirements. Example: maybe you are not working with critical information, or information is not updated regularly... Or maybe you have a slow server and you want to optimize resource usage...in those cases I think it's totally OK to prevent requests for a couple of minutes.

But if you need to always get the newest, what you are saying is perfectly valid. You can show the cached data first and, under the hood, fetch for new data and show a refresh button when there is new data to display

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ” Thread
 
kdfemi profile image
MiniSoda •

This is awesome, i will always put this in mind

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
gavinmagnus profile image
gavinmagnus •

Thanks for the article. I'm wondering about the situation when two separate components are requesting the same data at the same time. This way we would make two identical calls, because we cache only received data. Did you consider adding f.e. an additional, short-living cache for pending requests, so that any additional call would wait for the pending to finish instead?

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
laccastello profile image
Lucas Accastello •

Thanks for share Mauro. I think that your solution is clean and minimalist. I like it so much.

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
mauro_codes profile image
Mauro Garcia •

Thanks for your feedback Lucas! 😄

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
ravavyr profile image
Ravavyr •

There is a use for localStorage, but can we just call it that instead of "Client-side caching" ? It's localStorage, and it's also limited space, so with "caching" people will assume you mean it can store the entire site, which in my cases it cannot.

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
ribizlim profile image
Mark Magyarodi •

I don't see any reason to cache HTTP requests other than GET. For any backend modification the server needs to be contacted...

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
mauro_codes profile image
Mauro Garcia •

That's right! Maybe I could remove cacheMins from HttpOptions and add an additional param only for get requests.

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
pavodev profile image
Ivan Pavic •

Nice and clean solution! The customer of a project I'm working on has requested a caching system and I think I will try to implement this one!

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
mauro_codes profile image
Mauro Garcia •

Nice to hear that! I also have a mechanism in place to clean all cached queries every time I release a new version of my angular apps. I'm gonna be sharing my approach this week.

ðŸ”ī Hot News: mauro codes/client side caching with angular il - āļŠāļĢāļļāļ›āļ”āļĢāļēāļĄāđˆāļēāļĨāđˆāļēāļŠāļļāļ”
 
vishwamlr profile image
Vishwam Sirikonda •

here care should be taken we construct url appending httpParams encoded, there can be cases like /products?page=2