Mastering Custom Implementations of localStorage and sessionStorage

December, 3rd 2024 3 min read

In front-end development, localStorage and sessionStorage are widely used for storing client-side data.
However, in more advanced applications, you may need custom logic on top of their native behavior—such as encryption, logging, key protection, analytics, or multi-layered validation.

This guide shows how to elegantly rewrite storage methods without breaking native functionality, using proxies, hooks, and encapsulated patterns.


Why Override localStorage and sessionStorage?

You might need custom wrappers when:

  1. Business Logic — Special handling for certain keys (e.g., formatting, validation, encryption).
  2. Global Monitoring — Log every storage access for analytics or debugging.
  3. Security — Protect sensitive keys from modification or unauthorized reads.
  4. Cross‑Team Governance — Enforce consistent storage behavior across large applications.

Core Idea: Wrap, Don’t Replace

The goal is preserving original behavior while adding extra processing.

Step 1: Save the Native Methods

js
const _setItem = localStorage.setItem;
const _getItem = localStorage.getItem;

Step 2: Inject Custom Logic via Wrapper

js
localStorage.setItem = function (key, value) {
  if (key === "protectedKey") return; // Prevent modifications
  _setItem.call(this, key, value);
};

localStorage.getItem = function (key) {
  return key === "protectedKey" ? "Access denied" : _getItem.call(this, key);
};

This pattern keeps native functionality intact but adds rules for specific keys.


Flexible Hook-Based Proxy System

A more scalable approach uses configurable hooks.

js
function proxyStorage(storage, config = {}) {
  const {
    beforeSetItem = (k, v) => [k, v],
    afterGetItem = (k, v) => v,
  } = config;

  const _setItem = storage.setItem;
  const _getItem = storage.getItem;

  storage.setItem = function (key, value) {
    const [newKey, newValue] = beforeSetItem(key, value);
    _setItem.call(this, newKey, newValue);
  };

  storage.getItem = function (key) {
    const value = _getItem.call(this, key);
    return afterGetItem(key, value);
  };
}

Example Use Cases

1. Encrypt Storage Data

js
proxyStorage(localStorage, {
  beforeSetItem: (key, value) => [key, btoa(value)],
  afterGetItem: (key, value) => (value ? atob(value) : value),
});

2. Log Every Access

js
proxyStorage(sessionStorage, {
  beforeSetItem: (k, v) => {
    console.log("SET", k, v);
    return [k, v];
  },
  afterGetItem: (k, v) => {
    console.log("GET", k, v);
    return v;
  },
});

3. Prevent Access to Sensitive Keys

js
proxyStorage(localStorage, {
  afterGetItem: (key, value) =>
    key === "secret" ? null : value,
});

Encapsulation with a Singleton Class

This makes the proxy reusable and organized.

js
class StorageProxy {
  constructor(storage, config) {
    if (StorageProxy.instance) return StorageProxy.instance;
    this.init(storage, config);
    StorageProxy.instance = this;
  }

  init(storage, config) {
    this.storage = storage;
    this.originalSetItem = storage.setItem;
    this.originalGetItem = storage.getItem;
    this.config = config;
    this.proxy();
  }

  proxy() {
    const {
      storage,
      config: { beforeSetItem, afterGetItem },
    } = this;

    storage.setItem = (key, value) => {
      const [k, v] = beforeSetItem(key, value);
      this.originalSetItem.call(storage, k, v);
    };

    storage.getItem = key => {
      const value = this.originalGetItem.call(storage, key);
      return afterGetItem(key, value);
    };
  }

  unproxy() {
    this.storage.setItem = this.originalSetItem;
    this.storage.getItem = this.originalGetItem;
  }
}

Usage

js
const customStorage = new StorageProxy(localStorage, {
  beforeSetItem: (k, v) => [k, v.toUpperCase()],
  afterGetItem: (k, v) => v,
});

Summary

Rewriting localStorage and sessionStorage opens the door to:

  • Custom behavior
  • Security enhancements
  • Access monitoring
  • Data validation
  • Encryption and transformation

With wrappers, hooks, and patterns like singletons, you can tailor storage APIs while preserving the native browser functionality — making your front‑end architecture more secure, maintainable, and adaptable.