Web Storage API

Learn how to store data in the browser using localStorage and sessionStorage.

#browser

#storage

#intermediate

#persistence

As web applications grow more sophisticated, the need to store data on the client side becomes increasingly important. Whether you want to persist user settings, save application state between sessions, or cache data to reduce server requests, the Web Storage API provides a simple yet powerful solution.

What is Web Storage?

Web Storage is a set of browser APIs that allow you to store key-value pairs directly in the user’s browser. Unlike cookies, web storage data isn’t sent with every HTTP request, which means you can store larger amounts of data without impacting performance.

There are two main types of web storage:

  1. localStorage - Persists data even after the browser is closed and reopened
  2. sessionStorage - Maintains data for the duration of the page session (until the browser or tab is closed)

When to Use Web Storage

Web Storage is ideal for:

  • Saving user preferences
  • Storing form data temporarily
  • Caching API responses
  • Saving application state
  • Creating offline experiences
  • Reducing server requests

It is not suitable for:

  • Sensitive data (it’s not encrypted)
  • Very large datasets (typically limited to 5-10MB depending on the browser)
  • Data that needs to be accessed by the server

Basic Usage

Both localStorage and sessionStorage share the same API methods:

// Storing data
localStorage.setItem("username", "JohnDoe");

// Retrieving data
const username = localStorage.getItem("username");
console.log(username); // Output: JohnDoe

// Removing specific data
localStorage.removeItem("username");

// Clearing all data
localStorage.clear();

The difference is simply that sessionStorage data gets cleared when the session ends, while localStorage data persists indefinitely.

Storing Complex Data

The Web Storage API only stores strings. To store objects or arrays, you need to convert them to JSON:

// Storing an object
const user = {
  name: "John Doe",
  email: "john@example.com",
  preferences: {
    theme: "dark",
    fontSize: "medium",
  },
};

localStorage.setItem("user", JSON.stringify(user));

// Retrieving and parsing the object
const storedUser = JSON.parse(localStorage.getItem("user"));
console.log(storedUser.preferences.theme); // Output: dark

Storage Event

When localStorage changes in one tab, other tabs can be notified through the ‘storage’ event:

// Listen for changes to localStorage from other tabs/windows
window.addEventListener("storage", (event) => {
  console.log("Storage changed!");
  console.log("Key:", event.key);
  console.log("Old value:", event.oldValue);
  console.log("New value:", event.newValue);
  console.log("URL of page that made the change:", event.url);
});

This event only fires when localStorage is changed in another tab or window, not in the current one.

Storage Limits

Browsers typically allocate between 5-10MB of storage per domain for localStorage and sessionStorage combined. The exact limit varies by browser:

BrowserApproximate Limit
Chrome10MB
Firefox10MB
Safari5MB
Edge10MB

To check available space, you can use this function:

function getLocalStorageUsage() {
  let total = 0;
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    const value = localStorage.getItem(key);
    total += key.length + value.length;
  }

  // Convert to KB for readability
  const kilobytes = total / 1024;
  return {
    used: kilobytes.toFixed(2) + " KB",
    items: localStorage.length,
  };
}

console.log(getLocalStorageUsage());

Creating a Storage Wrapper

To make working with complex data easier, you can create a simple wrapper:

class StorageHelper {
  constructor(type = "local") {
    this.storage = type === "local" ? localStorage : sessionStorage;
  }

  setItem(key, value) {
    // Automatically stringify objects and arrays
    if (typeof value === "object") {
      value = JSON.stringify(value);
    }
    this.storage.setItem(key, value);
  }

  getItem(key) {
    const value = this.storage.getItem(key);

    // Try to parse JSON, but return the original value if it fails
    try {
      return JSON.parse(value);
    } catch (e) {
      return value;
    }
  }

  removeItem(key) {
    this.storage.removeItem(key);
  }

  clear() {
    this.storage.clear();
  }

  get length() {
    return this.storage.length;
  }

  key(index) {
    return this.storage.key(index);
  }
}

// Usage
const localStore = new StorageHelper("local");
const sessionStore = new StorageHelper("session");

localStore.setItem("user", { name: "John", age: 30 });
console.log(localStore.getItem("user")); // Output: {name: 'John', age: 30}

Practical Example: Theme Switcher

Here’s how you might use localStorage to remember a user’s theme preference:

// Theme switcher with localStorage
const themeToggle = document.getElementById("theme-toggle");
const body = document.body;

// Load saved theme on page load
document.addEventListener("DOMContentLoaded", () => {
  const savedTheme = localStorage.getItem("theme") || "light";
  body.classList.add(`theme-${savedTheme}`);

  // Update toggle to match saved theme
  if (themeToggle) {
    themeToggle.checked = savedTheme === "dark";
  }
});

// Handle theme toggle click
if (themeToggle) {
  themeToggle.addEventListener("change", () => {
    // Remove both theme classes
    body.classList.remove("theme-light", "theme-dark");

    // Add the appropriate theme class
    const newTheme = themeToggle.checked ? "dark" : "light";
    body.classList.add(`theme-${newTheme}`);

    // Save the preference
    localStorage.setItem("theme", newTheme);
  });
}

Form Data Persistence

Another common use is saving form data to prevent loss if the page is accidentally closed:

const form = document.getElementById("contact-form");
const formFields = form.querySelectorAll("input, textarea");
const formKey = "contact_form_draft";

// Save form data as the user types
formFields.forEach((field) => {
  // Load saved value if it exists
  const savedFormData = JSON.parse(localStorage.getItem(formKey) || "{}");
  if (field.name in savedFormData) {
    field.value = savedFormData[field.name];
  }

  // Save field value when it changes
  field.addEventListener("input", () => {
    const formData = JSON.parse(localStorage.getItem(formKey) || "{}");
    formData[field.name] = field.value;
    localStorage.setItem(formKey, JSON.stringify(formData));
  });
});

// Clear saved data when form is submitted
form.addEventListener("submit", () => {
  localStorage.removeItem(formKey);
});
B{Data to Store?}
B -->|Yes| C[Prepare Data]
C --> D[Convert to String/JSON]
D --> E[Save to Storage]
B -->|No| F[Need Stored Data?]
F -->|Yes| G[Retrieve from Storage]
G --> H[Parse if Needed]
H --> I[Use Data in App]
F -->|No| A
I --> A
E --> A

“ —>

Using IndexedDB for Larger Data

If you find yourself hitting storage limits, consider using IndexedDB, which allows much larger storage capacities:

// Basic IndexedDB example for comparison
function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open("MyAppDatabase", 1);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains("users")) {
        db.createObjectStore("users", { keyPath: "id" });
      }
    };

    request.onsuccess = (event) => {
      resolve(event.target.result);
    };

    request.onerror = (event) => {
      reject("Error opening database: " + event.target.error);
    };
  });
}

async function storeUser(user) {
  const db = await openDatabase();
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(["users"], "readwrite");
    const store = transaction.objectStore("users");
    const request = store.put(user);

    request.onsuccess = () => resolve(true);
    request.onerror = () => reject(request.error);
  });
}

// Usage
storeUser({ id: 1, name: "John", email: "john@example.com" })
  .then(() => console.log("User stored successfully"))
  .catch((error) => console.error("Error storing user:", error));

Security Considerations

Web Storage is convenient but has some security implications:

  1. Not encrypted - Don’t store sensitive data like passwords or credit card information
  2. Accessible via JavaScript - Any script running on your site can access storage
  3. XSS vulnerabilities - If your site has XSS vulnerabilities, attackers can steal storage data
  4. No httpOnly flag - Unlike cookies, you can’t make storage inaccessible to JavaScript

To mitigate risks:

// Consider encrypting sensitive data before storing
function encryptData(data, key) {
  // This is a very simple encryption example - use a proper library in production
  const encoded = btoa(JSON.stringify(data));
  return encoded;
}

function decryptData(encryptedData, key) {
  // Simple decryption example
  try {
    return JSON.parse(atob(encryptedData));
  } catch (e) {
    console.error("Decryption failed");
    return null;
  }
}

// Usage
const sensitiveData = { userId: 12345, accessLevel: "admin" };
localStorage.setItem("userData", encryptData(sensitiveData, "my-secret-key"));

Feature Detection and Fallbacks

Always check if Web Storage is available before using it:

function isStorageAvailable(type) {
  const storage = window[type];
  try {
    const testKey = "__storage_test__";
    storage.setItem(testKey, testKey);
    storage.removeItem(testKey);
    return true;
  } catch (e) {
    return false;
  }
}

// Usage
if (isStorageAvailable("localStorage")) {
  // localStorage is available
  localStorage.setItem("feature", "enabled");
} else {
  // Fallback to cookies or in-memory storage
  console.log("localStorage not available");
}

Conclusion

Web Storage provides a straightforward way to store data in the browser, enhancing user experience by preserving state across page loads or even browser sessions. With a solid understanding of localStorage and sessionStorage, you can build more responsive, user-friendly web applications that feel more like native apps.

Remember to use Web Storage judiciously, keeping in mind its limitations in terms of size and security. For larger or more sensitive data needs, consider alternatives like IndexedDB or server-side storage with proper authentication.