6.1 KiB
Tutorial 7: Persistence Patterns
This tutorial teaches both localStorage and IndexedDB persistence patterns for your reactive store.
“Saving Greetings” — Making the store survive reloads
🎯 Goal
Extend the helloStore so that all greetings persist between browser sessions.
When the page reloads, previously added greeters are restored automatically.
We’ll implement this in two steps:
- Start with simple
localStoragepersistence (fast, synchronous). - Upgrade to
IndexedDBfor larger data or structured offline storage.
Both variants will keep your TestDrive-UI environment self-contained and testable.
🧪 Step 1 — Write the failing tests
Create a new file:
src/store/hello-store.persistence.test.js
import { helloStore } from "./hello-store.js";
describe("helloStore persistence", () => {
beforeEach(() => {
// Clean simulated storage
global.localStorage = {
store: {},
getItem(k) { return this.store[k] || null; },
setItem(k, v) { this.store[k] = v; },
clear() { this.store = {}; }
};
});
it("saves state to localStorage after modification", () => {
helloStore.addGreeter("Persisty");
const saved = JSON.parse(localStorage.getItem("helloStore"));
expect(saved.greeters).to.include("Persisty");
});
it("restores saved state when re-initialized", () => {
localStorage.setItem(
"helloStore",
JSON.stringify({ greeters: ["Restored"] })
);
const { HelloStore } = await import("./hello-store.js");
const freshStore = new HelloStore();
const state = freshStore.getState();
expect(state.greeters).to.deep.equal(["Restored"]);
});
});
Run:
npm test
All tests will fail initially — perfect.
⚙️ Step 2 — Implement localStorage persistence
Open src/store/hello-store.js and extend it like this:
class HelloStore {
constructor() {
const saved = localStorage.getItem("helloStore");
this.state = saved ? JSON.parse(saved) : { greeters: ["World"] };
this.listeners = new Set();
}
getState() {
return this.state;
}
_notify() {
// Save current state before notifying
localStorage.setItem("helloStore", JSON.stringify(this.state));
for (const cb of this.listeners) cb(this.state);
}
subscribe(callback) {
this.listeners.add(callback);
callback(this.state);
return () => this.listeners.delete(callback);
}
addGreeter(name) {
this.state = { ...this.state, greeters: [...this.state.greeters, name] };
this._notify();
}
renameGreeter(oldName, newName) {
const updated = this.state.greeters.map((n) =>
n === oldName ? newName : n
);
this.state = { ...this.state, greeters: updated };
this._notify();
}
}
export const helloStore = new HelloStore();
export { HelloStore }; // exported for testing
✅ Run npm test again — tests should now pass.
🌐 Step 3 — Try it live
Start your dev server:
npm run dev
- Add new greeters in the dashboard.
- Reload the browser. → The same greeters reappear immediately!
🧱 Step 4 — Optional: Switch to IndexedDB for larger data
localStorage is fine for small JSON objects.
To persist more complex data or avoid blocking the main thread, you can use IndexedDB (via the tiny idb-keyval helper or native API).
Create src/store/storage.js:
// Minimal IndexedDB wrapper using idb-keyval style API
const DB_NAME = "testdrive-ui";
const STORE_NAME = "hello";
export async function saveState(state) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
req.onsuccess = () => {
const tx = req.result.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).put(state, "store");
tx.oncomplete = resolve;
tx.onerror = reject;
};
});
}
export async function loadState() {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE_NAME);
req.onsuccess = () => {
const tx = req.result.transaction(STORE_NAME, "readonly");
const get = tx.objectStore(STORE_NAME).get("store");
get.onsuccess = () => resolve(get.result);
get.onerror = reject;
};
});
}
Now update the store’s _notify() and constructor to use these async calls:
import { saveState, loadState } from "./storage.js";
class HelloStore {
constructor() {
this.listeners = new Set();
loadState().then(
(saved) =>
(this.state = saved || { greeters: ["World"] }) &&
this._notify()
);
}
async _notify() {
await saveState(this.state);
for (const cb of this.listeners) cb(this.state);
}
}
💡 Tip: because IndexedDB operations are async, wrap state changes in async methods and adjust your tests with await.
🧠 Step 5 — Testing IndexedDB logic
In Node environments, you can simulate IndexedDB with fake-indexeddb:
npm install --save-dev fake-indexeddb
Then in your test setup:
import "fake-indexeddb/auto";
Your tests now work without a browser.
✅ Outcome
Your TestDrive-UI environment now supports:
| Persistence Layer | Use Case | Pros |
|---|---|---|
| localStorage | Small, synchronous, quick persistence | Simple & immediate |
| IndexedDB | Large, structured, async persistence | Scalable & robust |
🔍 Reflection
Persistence transforms your store into a long-term memory layer. With this, TestDrive-UI crosses into progressive-web-app territory — offline-ready, recoverable, and reliable.
Continue to Tutorial 8 on Agent Automation.
xxx