webdev.complete
🪟 Browser APIs You Should Know
JavaScript
Lesson 36 of 117
20 min

Storage: localStorage to IndexedDB

What to use for what, cookie vs storage, persistence rules.

The browser gives you four real ways to remember data between page loads: localStorage, sessionStorage, cookies, and IndexedDB. Each has a sweet spot, and the wrong choice is a footgun (slow page, gone data, security leak). Let's sort them out.

localStorage: the simple one

js
// Strings only - JSON your way in and out
localStorage.setItem("theme", "dark");
localStorage.getItem("theme");        // "dark"
localStorage.removeItem("theme");
localStorage.clear();                  // wipe everything

// Store objects via JSON
localStorage.setItem("user", JSON.stringify({ name: "Ada", id: 1 }));
const user = JSON.parse(localStorage.getItem("user") ?? "null");

The big facts you have to know about localStorage:

  • Per origin. Each site has its own bucket.
  • ~5 MB limit in most browsers. Throws when full.
  • Synchronous. Reads block the main thread. Avoid in tight loops or for large data.
  • Strings only. Anything else is implicitly coerced. JSON.stringify it.
  • Persists forever until the user clears site data or your code deletes it.

sessionStorage: identical API, gone when the tab closes

js
sessionStorage.setItem("draft", "unsent message");

// Same methods as localStorage. Scoped to the tab.
// Closing the tab clears it. Refreshing keeps it.

Same shape as localStorage but data dies with the tab. Great for unsent form drafts, multi-step wizards, or anything that shouldn't outlive the visit.

Cookies: the awkward grandparent

Cookies are key-value pairs the browser sends with every HTTP request to the same origin. That's their superpower and their downside:

js
// JS access (rarely what you want now)
document.cookie = "theme=dark; max-age=31536000; path=/; SameSite=Lax";

// Reading is gross - gives you "a=1; b=2; c=3"
const all = document.cookie;
  • ~4 KB per cookie. Very tight budget.
  • Sent on every request. Bloats network traffic.
  • Use HttpOnlycookies (set by the server, invisible to JS) for auth tokens. That's their real job today.
  • Use Secure + SameSite attributes to protect against CSRF and leaks.
Don't put tokens in localStorage
A common mistake: storing auth tokens in localStorage because cookies feel old. Any third-party script that runs on your page can read localStorage. HttpOnly cookies are invisible to JS by design. Use cookies for credentials.

IndexedDB: real database in the browser

When you need megabytes (or gigabytes) of structured data, querying, indexes, and async access, that's IndexedDB. The native API is famously clunky, so most people use a tiny wrapper like idb-keyval or Dexie:

js
// Conceptual sketch (real code uses callbacks and events)
const db = await openDB("notes", 1, {
  upgrade(db) { db.createObjectStore("items", { keyPath: "id" }); },
});

await db.put("items", { id: 1, title: "hi", body: "..." });
const item = await db.get("items", 1);
const all  = await db.getAll("items");

Use IndexedDB when you have offline support, file caching, large datasets, or anything beyond "remember a few preferences."

Decision flowchart

  • A few KB of UI prefs that should persist? localStorage
  • Data scoped to one tab/visit? sessionStorage
  • Auth tokens or session IDs? HttpOnly cookie (server-set)
  • Megabytes, structured, offline-capable? IndexedDB
  • Server-side cache? Not the browser's job

A reusable localStorage helper

js
const store = {
  get(key, fallback = null) {
    try {
      const raw = localStorage.getItem(key);
      return raw === null ? fallback : JSON.parse(raw);
    } catch {
      return fallback;
    }
  },
  set(key, value) {
    try { localStorage.setItem(key, JSON.stringify(value)); }
    catch (err) { console.warn("localStorage full or blocked:", err); }
  },
  remove(key) { localStorage.removeItem(key); },
};

store.set("user", { name: "Ada" });
store.get("user", {});                 // { name: "Ada" }
store.get("missing", "default");      // "default"

Try it: remember the name

A name input that remembers you across refreshes. Type a name, refresh the playground, watch it persist. Bonus: try emptying it and refreshing.

const KEY = "remember-name";

const input = document.getElementById("name");
const greet = document.getElementById("greet");
const clearBtn = document.getElementById("clear");

// Tiny helper
const store = {
  get(key, fallback = null) {
    try { return JSON.parse(localStorage.getItem(key)) ?? fallback; }
    catch { return fallback; }
  },
  set(key, value) {
    try { localStorage.setItem(key, JSON.stringify(value)); }
    catch (err) { console.warn(err); }
  },
  remove(key) { localStorage.removeItem(key); },
};

// Restore on load
const saved = store.get(KEY, "");
input.value = saved;
render();

input.addEventListener("input", () => {
  store.set(KEY, input.value);
  render();
});

clearBtn.addEventListener("click", () => {
  store.remove(KEY);
  input.value = "";
  render();
});

function render() {
  greet.textContent = input.value
    ? "Hello, " + input.value + "!"
    : "Type your name above.";
}

console.log("loaded value:", saved);

Quiz

Quiz1 / 3

Roughly how much can you store in localStorage per origin?

Recap

  • localStorage: 5 MB, synchronous, strings only, persists forever, per-origin.
  • sessionStorage: same API, dies with the tab.
  • Cookies: tiny (4 KB), sent on every request, use HttpOnly for auth.
  • IndexedDB: real database, async, big, structured. Use a wrapper.
  • Never store auth tokens in localStorage. XSS will steal them.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.