# 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**: 1. Start with simple **`localStorage`** persistence (fast, synchronous). 2. Upgrade to **`IndexedDB`** for 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` ```javascript 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: ```bash npm test ``` All tests will fail initially — perfect. --- ## ⚙️ Step 2 — Implement `localStorage` persistence Open `src/store/hello-store.js` and extend it like this: ```javascript 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: ```bash npm run dev ``` 1. Add new greeters in the dashboard. 2. 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`: ```javascript // 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: ```javascript 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`: ```bash npm install --save-dev fake-indexeddb ``` Then in your test setup: ```javascript 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