Add browser storage to your js or ts service
mini-stash is a lightweight npm package responsible for streamlining local and session storage. The package page has all the information and examples needed to get up and running quickly. Read on if you’d like a better understand of how caching is implemented in a JavaScript or TypeScript project.
Local and session storage in a nutshell
When it comes to browser storage (excluding cookies) there are 3 routine actions: saving, retrieving, and clearing. Within these actions we have 4 basic considerations.
Session or local storage
We choose between local or session storage depending on the nature, lifespan, and sensitivity of data.
- Local Storage
- maintained between sessions (i.e. it stays after you close the browser)
- Session Storage
- cleared automatically between sessions (i.e. when you close the browser)
Choosing a storage key
Data is saved into browser storage with an associated key or string so it can be retrieved later. Since there’s nothing to stop another feature on the site from using the same key, we should make ours as specific as possible.
good key: myapp-footer-primary-links
bad key: links
Storing complex data
Saving primitive data to the browser like numbers and strings just works, but you’ll run into issues when storing or retrieving objects. To get around this, saving should incorporate JSON.stringify
and retrieving should incorporate JSON.parse
.
Expiration
Unlike cookies, local and session storage don’t incorporate any expiration logic out of the box. To work-in expiration we also need to save a date timestamp. The timestamp could be paired directly with the save-data (i.e. saving an object with date and data properties) or it could be saved as a separate entry with a standardized naming convention (i.e. if ‘myData’ stores the data ‘myData_date’ will store the date). I prefer the latter option in order to keep data agnostic from the retrieval method.
High level example
This is a simplified example that shows the high level concept for each action: save, retrieve, and clear. Later examples show full working code. Note: adding the +
operator to date object will convert it to number.
Saving to local or session storage
Saving is fairly straightforward. The key consideration is to JSON.stringify
data before saving in order to support objects. Additionally, we save the date of storage separately with a standardized naming convention.
I consider local storage the more frequent use case so the useSession
boolean parameter is optional.
function save(key, data, useSession) {
// json stringify data
const saveData = JSON.stringify(data);
// save to local or session storage
if (useSesson) {
sessionStorage.setItem(key, saveData);
// store date
sessionStorage.setItem(key + '_date', +(new Date());
} else {
localStorage.setItem(key, saveData);
// store date
localStorage.setItem(key + '_date', +(new Date());
}
}
Retrieving from local or session storage
Note, if data is retrieved we need to JSON.parse
it since we saved a json string.
The expirationMinutes
parameter indicates the number of minutes from time of storage before data is considered expired. If data is expired or if I can’t find an associated date I like to tidy up by clearing the data from storage.
function retrieve(key, expirationMinutes, useSession) {
let data, date;
if (useSession) {
data = sessionStorage.getItem(key);
date = sessionStorage.getItem(key + '_date');
} else {
data = localStorage.getItem(key);
date = localStorage.getItem(key + '_date');
}
// parse json data
data = JSON.parse(data);
// return data if no expiration
if (!expirationDate || expirationDate <= 0) {
return date;
}
// determine if expired
const expired = false; // ... see example later on
// clear expired data
if (expired || !date) {
data = null;
clear(key, useSession);
} else {
return data;
}
}
Clearing data from local or session storage
Clearing local or session storage utilizes the delete
operator. Since we store the date separately, we should clear this as well as the primary data.
function clear(key, useSession) {
if (useSession) {
delete sessionStorage[key];
delete sessionStorage[key + '_date'];
} else {
delete localStorage[key];
delete localStroage[key + '_date'];
}
}
Using caching in a service
There’s a basic pattern to follow for implementing caching in a service.
- Check for the presence of cache data, indicating any expiration details.
- If the data isn’t retrieved or if it has expired, get the data the normal way.
- Before using the newly retrieved data, save a copy to storage.
For reuse, you could save these utilities to a separate file and import them in services as needed. Another technique I’ve used frequently is to create a base class which includes all caching functionality. Service classes can extend this class in order to standardize caching for services in an app.
Note: here’s a full implementation for checking expiration (written in TypeScript).
ApiStorageBase.ts:
export interface IStorage {
getItem(key:string):any;
setItem(key:string, data:any):void;
}
export default class ApiStorageBase {
protected save = (key:string, data: any, useSessionStorage: boolean = false):void => {
const storage = this.getStorage(useSessionStorage);
const time = Math.floor(new Date().getTime() / 1000);
// store data as json string
storage.setItem(key, JSON.stringify(data));
// store unix date string for expiration check
storage.setItem(this.getDateKey(key), time.toString());
}
protected retrieve = (
key: string,
expirationMinutes: number,
useSessionStorage: boolean = false
): any => {
const storage = this.getStorage(useSessionStorage);
const storageDate = +storage.getItem(this.getDateKey(key));
let data = storage.getItem(key);
if (!!data) {
// check if expired
const expires = minutesToExpiration > 0;
let expired;
if (expires && !!storageDate) {
const nowMs = +(new Date());
const storageMs = +(new Date(storageDate)) * 1000;
const minutesOld = Math.ceil(
(nowMs - storageMs) / 1000 / 60
);
expired = minutesOld >= minutesToExpiration;
}
if (expires && (!storageDate || expired)) {
// discard if expired or missing date
this.clear(storageKey, useSessionStorage);
data = null;
} else {
// parse json
try {
data = JSON.parse(data);
} catch (e) {
// raw data will be retrieved if problem parsing
}
}
}
return data;
}
protected clear = (key: string, useSessionStorage: boolean = false): void => {
const storage = this.getStorage(useSessionStorage);
// delete the data and the storage date
delete storage[key];
delete storage[getDateKey(key)];
}
private getStorage = (useSessionStorage: boolean = false):IStorage => !useSessionStorage
? localStorage
: sessionStorage;
private getDateKey = (key: string):string => `${key}_date}`;
}
It might seem tempting to require a storage key in the constructor of ApiStorageBase. This would make save and retrieve even more streamlined, however, the moment a service adds a second data point, the usage will become confused.
You might note that moment.js’s query methods can simply the expiration check. While true, adding moment would significantly increase the overall size of our bundled code. You don’t need MomentJs.
Local and session storage npm package
The base class method above is an easy way to standardize services, but one-off utilities are more versatile. A small-scale npm package would be an even better approach for packaging these utilities since this would avoid code duplication and better handle updates between projects.
mini-stash
is a package I created for this purpose. The source code is nearly identical to the above examples.
Check out the example in the project’s readme for more info.