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

angular javascript webdev ux

Series:

1Client-side caching with Angular

2Client-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
	}
]

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
	}

}

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)
		}
	}

}

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
}

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
}

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()
	}
}

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.


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