The request
"I refreshed the page and lost my work." Before choosing storage, ask what must survive, for whom, and on which device. Our decision: remember one unfinished draft in this browser. A local save is neither a confirmed order nor a backup shared across devices.
Store inputs, recompute derived results
Persist a versioned envelope containing the draft, not the displayed total or an HTML fragment. On restoration, validate its shape and business inputs, then calculate a fresh quote. The saved value can be old, manually modified or incompatible with the current rules.
Use order-workbench:draft:v1 as the key. Keep the version inside the value too, so exports or renamed keys cannot disguise an incompatible format. Do not store credentials or real customer information in this exercise.
New boundary: objects become stored text
Browser local storage stores strings. JSON is a text format for data: JSON.stringify turns our plain draft object into JSON text, and JSON.parse reads that text back into a value. Neither operation checks the order's business rules. Parsing malformed JSON throws; successfully parsing an object with a negative quantity still requires validation.
The envelope groups the format version and draft. setItem(key, text) replaces the value under that key; getItem(key) returns its text or null when missing. These methods can fail, which is why the adapter catches errors. In saved?.version, optional chaining returns undefined if saved is null or undefined instead of trying to access its property. It does not validate the rest of the data.
Create storage.js. These functions accept a storage object so tests can supply a fake. validateDraft is your function: it must reject non-drafts, invalid customer/item fields and invalid pricing inputs, returning normally only for an acceptable draft.
export function saveDraft(storage, draft, validateDraft) {
try {
validateDraft(draft);
storage.setItem('order-workbench:draft:v1', JSON.stringify({
version: 1, draft
}));
return { ok: true };
} catch {
return { ok: false, message: 'Draft could not be saved on this device.' };
}
}
export function loadDraft(storage, validateDraft) {
try {
const raw = storage.getItem('order-workbench:draft:v1');
if (raw === null) return { kind: 'missing' };
const saved = JSON.parse(raw);
if (saved?.version !== 1) return { kind: 'invalid' };
validateDraft(saved.draft);
return { kind: 'ready', draft: saved.draft };
} catch {
return { kind: 'invalid' };
}
}
Access to window.localStorage itself may throw. Put that access inside a caller's try/catch too; if unavailable, keep the editor usable in memory and explain that saving is unavailable. Never display "Saved" before setItem succeeds.
Integration steps
- On startup, load once before rendering. Missing means a new draft; invalid means offer a new draft with an explanatory message.
- Add an explicit Save draft action. Save only normalized, valid domain data. Invalid form text remains visible for correction but is not silently persisted.
- Track
hasUnsavedChangesindependently of quote validity. Clear it only after a successful save. - A confirmed or cancelled order must not be restored as an editable draft. Remove this draft key after a successful simulated transition, reporting storage failure separately.
Break your implementation
Try invalid JSON, version 2, a negative quantity and a storage fake whose setItem throws. In every case the page must remain usable. Reload after saving quantity 2: expect the inputs restored and 324.00 for the premium example. Change the pricing policy locally: restoration must use the current calculator, not yesterday's total.
Record the limitation: browser data can be cleared. See MDN's localStorage reference for origin scope and exceptions.