Persisting User Data in a No-Backend HTML App (localStorage Patterns)
A single-file offline tool has a hard problem: how do you remember anything between sessions with no backend? The answer is localStorage — but naively used, it throws in exactly the environments where a downloaded file often runs. Here's a pattern that persists user data reliably and degrades gracefully when it can't.
The problem with naive localStorage
localStorage.setItem throws in several real situations: private browsing, certain sandboxed contexts, and some configurations of file:// origins — which is precisely how a downloaded HTML tool gets opened. If your save logic isn't wrapped, one throw takes down the feature (or the whole script).
The pattern: wrap and fall back
Wrap every storage call, and fall back to an in-memory object when it throws. The feature keeps working within the session even when persistence isn't available:
const MEM = {};
function store(k, v){
try { localStorage.setItem(k, v); }
catch(e) { MEM[k] = v; }
}
function load(k){
try {
const x = localStorage.getItem(k);
if (x !== null) return x;
} catch(e) {}
return (k in MEM) ? MEM[k] : null;
}
Now store/load never throw. Best case, data persists across sessions. Worst case, it persists for the current session. Either way the UI never breaks.
Serializing collections
Favorites are a Set; history is an array. Serialize on write, parse on read, and guard the parse:
let favSet = new Set();
try { favSet = new Set(JSON.parse(load("favs") || "[]")); } catch(e) {}
function saveFavs(){ store("favs", JSON.stringify([...favSet])); }
The || "[]" and the try/catch mean a missing or corrupted key yields an empty set instead of a crash.
Capping growth
History that grows forever will eventually hit storage quotas. Cap it on write:
function pushHist(txt){
hist = [txt, ...hist.filter(h => h !== txt)].slice(0, 20);
store("hist", JSON.stringify(hist));
}
De-dupe (remove existing copy), prepend (newest first), slice (cap at 20). One line does all three.
The UX caveat worth documenting
localStorage is keyed to the file's origin/path. If the user moves the file, the data may not follow. Worth telling users to keep the file in a stable location — a small note that prevents a "my favorites vanished" support ticket.
See it working
The free Seedance Prompt Composer demo uses exactly this pattern for its history — open it, copy a few prompts, reload, and they're still there. Crack open the source to see the fallback in context.
Top comments (0)