This continues the browser storage series, following up from "Browser storage: How to use browser storage code examples". Navigate there to see a high level overview of saving, retrieving, and clearing.

Handle caching in a service responsible for retrieving data

Moving caching logic into service can standardize caching implementation in an app and help reduce code duplication.

There's a basic pattern to follow for implementing caching in a service.

  1. Check for the presence of cache data, indicating any expiration details.
  2. If the data isn't retrieved or if it has expired, get the data the normal way.
  3. 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 using module syntax. 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: included below is a full implementation for checking expiration (written in TypeScript) which wasn't fleshed out in the previous post.

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.

Blog post series

Continuing the series is a post on my lightweight npm package eliminating the boilerplate code we've already outlined. This cuts browser storage implementation down to a few lines which no longer need to be copied from project to project: Browser storage: mini-stash npm package.


Last updated