Docs

Client-Side Data Caching

Understanding when and how to cache client-side data.

Hilla caches client-side views out of the box, but not client-side data. This can even lead to a situation where the client-side view is available offline, which isn’t very useful, since there is no data. To overcome this, you can use localStorage to store the client-side data, so that the data is available even offline.

Cacheable Wrapper with Local Storage

Assume you have a function to retrieve the data from the backend. After the data is retrieved, you can save it into localStorage as a key-value pair. It’s also good to provide a default value, in case the data is available from neither the backend nor cache.

Example: define a cacheable helper in cacheable.ts to cache client-side data.

export async function cacheable<T>(fn: () => Promise<T>, key: string, defaultValue: T) {
  let result;
  try {
    // retrieve the data from backend.
    result = await fn();
    // save the data to localStorage.
    localStorage.setItem(key, JSON.stringify(result));
  } catch {
    // if failed to retrieve the data from backend, try localStorage.
    const cached = localStorage.getItem(key);
    // use the cached data if available, otherwise the default value.
    result = cached ? JSON.parse(cached) : defaultValue;
  }

  return result;
}

Now you can use the cacheable wrapper in your client-side view.

Example: use the cacheable wrapper.

// import the cacheable function from the path to cacheable.ts file (without the suffix)
import { cacheable } from 'path-to-cacheable';

// instead of always relying on the backend to retrieve the data. e.g.
// const stats = await getStats();
// you can now wrap getStats into the cacheable helper.
const stats = await cacheable(() => getStats(), 'stats', {});

Clear the Local Storage

It’s good practice to clear the data stored in the localStorage, for example when a user logs out. Suppose there is a logout event. The above cacheable wrapper could be improved as:

// the name of the cache for offline usage
const CACHE_NAME = 'offline-cache';

window.addEventListener('logout', () => {
  // clear the data from localStorage when a user logs out
  clearCache();
});

export async function cacheable<T>(fn: () => Promise<T>, key: string, defaultValue: T) {
  let result;
  try {
    // retrieve the data from backend.
    result = await fn();
    // save the data to localStorage.
    const cache = getCache();
    cache[key] = result;
    localStorage.setItem(CACHE_NAME, JSON.stringify(cache));
  } catch {
    // if failed to retrieve the data from backend, try localStorage.
    const cache = getCache();
    const cached = cache[key];
    // use the cached data if available, otherwise the default value.
    result = cached === undefined ? defaultValue : cached;
  }

  return result;
}

function getCache(): any {
  const cache = localStorage.getItem(CACHE_NAME) || '{}';
  return JSON.parse(cache);
}

function clearCache() {
  localStorage.removeItem(CACHE_NAME);
}