DEV Community

Mauro Garcia
Mauro Garcia

Posted on • Edited on • Originally published at maurogarcia.dev

Client-side caching with Angular (Part 2) - Versioning

Last week I wrote an article about how I handle client-side caching with Angular:

But let's say we just released the first version of out app and we're retrieving a list of products in the home page. Currently, our products have the following properties:

  • name
  • description
  • price

So, our cached query results look like this:

[
    {
        "name": "product 1",
        "description": "description for product 1",
        "price": 100
    },
    {
        "name": "product 2",
        "description": "description for product 2",
        "price": 150
    },
    {
        "name": "product 3",
        "description": "description for product 3",
        "price": 200
    }
]
Enter fullscreen mode Exit fullscreen mode

Now, let's say we realised that we were missing a required property called "available" (it's a boolean).

We update our angular component to include the new property (I'm assuming that our API was updated too and it's retrieving the new property as well).

Finally, we publish the new version of our app.

Problem

One common problem we could face when working with cached data is that some of our clients will still have the old version of the products query being retrieved from localStorage. This could lead to unexpected errors because we're assuming that the new property will always be available (as it's required).

Solution

In this article I'm gonna share my approach to cleanup the localStorage every time I release a new version of my angular apps. In that way, my clients will always get a valid version of my queries without loosing our cache capabilities.

This solution have 3 steps:
1 - Create a list of cached queries we want to clean after each release
2 - Check if our user has an older version of our app
3 - Go through each cached query (using the list created in the first step above) and remove it from localStorage.

All this steps will be handled by our brand new System Service:

import { Injectable } from '@angular/core'
import { CacheService } from './cache.service'
import { environment } from 'src/environments/environment'

@Injectable()
export class SystemService {

    // List of cached queries that'll removed from localStorage after each new release
    cachedQueries = {
        PRODUCT_LIST: `${environment.API_DOMAIN}/product`,
        CATEGORY_LIST: `${environment.API_DOMAIN}/category`,
    }
    versionCookie = "[AppName]-version"

    constructor(
        private _cacheService: CacheService
    ) { }

    checkVersion() {
        if (this.userHasOlderVersion()) {
            // Set new version
            this._cacheService.save({ key: this.versionCookie, data: environment.VERSION })
            // Cleanup cached queries to avoid inconsistencies
            this._cacheService.cleanCachedQueries(this.cachedQueries)
        }
    }

    userHasOlderVersion(): boolean {
        const userVersion = this._cacheService.load({ key: this.versionCookie })

        if (userVersion === null) {
            return true
        }

        return userVersion !== environment.VERSION
    }

}

Enter fullscreen mode Exit fullscreen mode

As you can see, I'm using the Cache service I created in my last article. But I'm also adding a new method called cleanCachedQueries:

import { Injectable } from '@angular/core'

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

    // If you need the full version of this service, please checkout my previous article.

    cleanCachedQueries(queries: Object) {
        queries = Object.values(queries)

        for (const query of queries) {
            localStorage.removeItem(query)
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

One more thing to notice is that I'm getting the version of my app from my environment file:

// environment.ts
import { version } from '../../package.json'

export const environment = {
    production: false,
    API_DOMAIN: 'https://example.com/api',
    VERSION: version
}
Enter fullscreen mode Exit fullscreen mode

Important

As you can see, I'm getting the current version of my app from the package.json file. So it's important that you remember to update your app version before each new release.

We'll also need to add the new typescript compiler option called resolveJsonModule in our tsconfig.app.json file to be able to read our package.json file to get the version of our app:

"compilerOptions": {
        "resolveJsonModule": true
}
Enter fullscreen mode Exit fullscreen mode

Checking the app version

Last but not least, we'll add just one line of code in our app.component.ts to check the app version and remove our old cached queries:

import { Component, OnInit } from '@angular/core'
import { SystemService } from './services/system.service'

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
    title = 'Your App'
    showNavbar = true
    constructor(
        private _systemService: SystemService,
    ) { }

    ngOnInit(): void {
        this._systemService.checkVersion()
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it. Now, every time you release a new version of your app, you'll only have to remember to update your app version in the package.json file and keep you cachedQueries list up to date. The System service will take care of the rest.


Let me know what do you think about this approach.

How do you handle this kind of incompatibilities after each release when dealing with cached queries?

Let me know in the comments below 👇

Top comments (2)

Collapse
 
sonicoder profile image
Gábor Soós

Have you considered this scenario?

  • can use the new field if present
  • purge cache if the field is not present and re-request it
  • remove the handling when the field is not present

This way, you have three separate releases instead of one, but the technique can be applied to many kinds of breaking change.

Collapse
 
mauro_codes profile image
Mauro Garcia

I'm not sure I'm understanding your approach. When you talk about having a temporal handling do you mean adding a temporal logic in the components to ask if the value is present?