Saving data: localStorage and sessionStorage
Web pages forget everything when they're closed or reloaded, unless they save it somewhere. The Web Storage API gives every website two small key-value stores inside the browser: localStorage, which keeps data until it's deleted (even after the browser restarts), and sessionStorage, which keeps it only while the tab is open. They're how sites remember your dark-mode choice, a half-written comment, or a shopping cart without an account. CodeMIRAI keeps your progress in localStorage this way.
The basics
Both stores have the same methods, and both store strings only, under string keys:
| Method | Does |
|---|---|
setItem(key, value) | saves a value (turned into a string) |
getItem(key) | reads it, or null if there's nothing under that key |
removeItem(key) | deletes one key |
clear() | deletes everything this site stored |
length / key(i) | how many keys / the name of the i-th key |
<script>
localStorage.setItem("username", "Asha");
localStorage.setItem("visits", 3); // stored as the string "3"
console.log(localStorage.getItem("username"));
console.log(typeof localStorage.getItem("visits")); // string!
console.log(localStorage.getItem("missing")); // null when nothing is saved
localStorage.removeItem("username");
console.log(localStorage.length, "key left:", localStorage.key(0));
</script>Everything comes back as a string, so convert numbers with Number(), and remember that a missing key gives null.
Storing objects and arrays with JSON
To save an object or array, turn it into JSON text with JSON.stringify, and turn it back with JSON.parse:
<script>
const settings = { theme: "dark", fontSize: 16, languages: ["js", "py"] };
localStorage.setItem("settings", JSON.stringify(settings));
const saved = JSON.parse(localStorage.getItem("settings"));
console.log(saved.theme, saved.languages.length);
localStorage.setItem("broken", { theme: "dark" }); // without stringify...
console.log(localStorage.getItem("broken")); // ...it's saved as "[object Object]"
</script>Reading safely
Saved data can be missing (first visit), out of date, or damaged. A small helper that falls back to a default keeps the rest of your code simple:
<script>
function load(key, fallback) {
try {
const text = localStorage.getItem(key);
return text === null ? fallback : JSON.parse(text);
} catch {
return fallback; // broken JSON, or storage blocked
}
}
function save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.log("couldn't save:", err.name); // full, or blocked in private mode
}
}
localStorage.setItem("cart", "{oops"); // pretend it got damaged
console.log(load("cart", []), load("prefs", { theme: "light" }));
save("cart", [{ item: "pen", qty: 2 }]);
console.log(load("cart", []));
</script>getItem and setItem can also throw: when storage is full, or when the browser blocks it (some private modes, or a site's privacy settings). Wrap them in try/catch in real projects.
Example: remembering a theme
<style>
body { font-family: system-ui; padding: 12px; transition: background .2s; }
body.dark { background: #111827; color: #e5e7eb; }
</style>
<button id="toggle">Toggle dark mode</button>
<p>Reload the page on a real site and the choice stays.</p>
<script>
const KEY = "theme";
if (localStorage.getItem(KEY) === "dark") document.body.classList.add("dark"); // apply the saved choice
document.querySelector("#toggle").addEventListener("click", () => {
const dark = document.body.classList.toggle("dark");
localStorage.setItem(KEY, dark ? "dark" : "light");
console.log("saved theme:", localStorage.getItem(KEY));
});
</script>Example: a visit counter and a saved draft
<p id="visits"></p>
<textarea id="draft" rows="3" cols="40" placeholder="Start typing: it's saved as you go"></textarea>
<p id="saved" style="font: 12px system-ui; color: gray"></p>
<script>
const visits = Number(localStorage.getItem("visits") ?? 0) + 1;
localStorage.setItem("visits", visits);
document.querySelector("#visits").textContent = `You've opened this page ${visits} time(s).`;
const draft = document.querySelector("#draft");
draft.value = localStorage.getItem("draft") ?? "";
draft.addEventListener("input", () => {
localStorage.setItem("draft", draft.value);
document.querySelector("#saved").textContent = "Draft saved " + new Date().toLocaleTimeString();
});
</script>Example: a to-do list that's remembered
The usual pattern: load the data once at the start, change the data, then save and redraw after every change:
<form id="f"><input id="t" placeholder="New task" required> <button>Add</button></form>
<ul id="list"></ul>
<script>
const KEY = "todos";
let todos = JSON.parse(localStorage.getItem(KEY) ?? '["Read about localStorage"]');
function commit() {
localStorage.setItem(KEY, JSON.stringify(todos));
const ul = document.querySelector("#list");
ul.replaceChildren(...todos.map((text, i) => {
const li = document.createElement("li");
li.textContent = text + " ";
const x = document.createElement("button");
x.textContent = "done";
x.addEventListener("click", () => { todos.splice(i, 1); commit(); });
li.append(x);
return li;
}));
}
document.querySelector("#f").addEventListener("submit", (e) => {
e.preventDefault();
todos.push(document.querySelector("#t").value.trim());
document.querySelector("#t").value = "";
commit();
});
commit();
console.log("stored:", localStorage.getItem(KEY));
</script>localStorage or sessionStorage?
localStorage | sessionStorage | |
|---|---|---|
| Lasts | until deleted (survives restarts) | until the tab is closed |
| Shared between | all tabs of the same site | only this tab |
| Good for | preferences, saved progress, drafts | a multi-step form in one tab, one-time messages |
Both are tied to the origin (protocol + domain + port): https://codemirai.com can't read another site's storage, and http:// and https:// versions of a site are separate.
The storage event: other tabs
When one tab changes localStorage, other tabs of the same site get a storage event, which lets them stay in sync (for example, logging out everywhere at once):
window.addEventListener("storage", (e) => {
console.log(`${e.key} changed from ${e.oldValue} to ${e.newValue}`);
if (e.key === "theme") document.body.classList.toggle("dark", e.newValue === "dark");
});The tab that made the change doesn't receive the event, only the others.
Limits and what not to store
- About 5 MB per site. Fine for settings and small data; use IndexedDB for more.
- Synchronous. Reads and writes pause the page briefly, so don't save huge values on every keystroke.
- Not secure. Any JavaScript running on the page can read it, including an attacker's script if the site has an XSS hole. Never store passwords, card numbers or secret tokens in it.
- Per browser, per device. Data saved on a laptop isn't on the phone. Syncing needs a server and an account (that's what CodeMIRAI's sign-in adds).
- Users can clear it at any time, so treat it as a convenience, not the only copy of important data.
Other ways to keep data
| Size | Sent to the server | Use for | |
|---|---|---|---|
localStorage / sessionStorage | about 5 MB | no | settings, drafts, small app data |
| cookies | about 4 KB | with every request | sign-in sessions (set by the server, ideally HttpOnly) |
| IndexedDB | large (hundreds of MB) | no | big or structured data, offline apps, files |
| a server database | unlimited | (it is the server) | anything shared between devices or people |
Key points
localStoragekeeps data across visits;sessionStorageonly for the tab's life.- Both store strings:
setItem,getItem(givesnullif missing),removeItem,clear. - Store objects with
JSON.stringifyand read them back withJSON.parse, insidetry/catch. - Load once at start, save after every change.
- Storage is per site, per browser, about 5 MB, readable by any script on the page: no secrets.
Practice
- Save the user's name from an input, and greet them by name the next time the page loads.
- Make a counter with + and - buttons whose value is saved in
localStorage. - Save an array of favourite colours, add one, and read the array back.
- Write
clearIfOlderThan(key, days)that stores a value with a saved date and deletes it once it's too old.