A Common User Experience Frustration
Everyone has faced this at some point – you’re halfway through filling out a form, adding products to a cart, or writing a note online, and then the tab closes by mistake. All your work disappears. A “Save for Later” feature prevents this by storing your progress in the browser, letting you return and continue without starting over.
In this blog, we’ll explore two powerful browser storage options for implementing this feature :
- Local Storage (simple, best for small key-value data)
- IndexedDB (robust, best for structured or large data)
Option 1 : Save for Later with Local Storage
What is Local Storage?
Local Storage is a synchronous key-value store in the browser that keeps the data even after a page refresh or browser restart. It is helpful for small pieces of data.
- Size limit : ~5MB per origin
- Data type : Strings only (objects need to be stringified)
- API : setItem, getItem, removeItem
Advantages of Local Storage
- Very easy to use (simple API).
- Data persists even after page reload or browser restart.
- Great for quick key-value storage.
- No need for external libraries.
Disadvantages of Local Storage
- Synchronous → large operations can block the main thread.
- Stores only strings (objects need JSON.stringify).
- Not secure (accessible via JavaScript → XSS risk).
- Limited size (~5MB).
Limitations
- No querying, indexing, or transactions.
- Cannot handle large structured data.
- Not suitable for sensitive data.
Where to Use Local Storage
- Saving form drafts (like a signup form).
- Storing user preferences (theme, language).
- Keeping filter/search states.
- Remembering UI settings (sidebar open/closed).
Full React Code Example
You can just drop this code into a React app (like App.js in Create React App or Vite). It handles saving, loading, and clearing form data using Local Storage.
// App.tsx
import React, { useState, useEffect } from "react";
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem extends Product {
quantity: number;
}
const STORAGE_KEY = "shopping_cart";
function App() {
const products: Product[] = [
{ id: 1, name: "Apple", price: 2 },
{ id: 2, name: "Banana", price: 1 },
{ id: 3, name: "Orange", price: 3 },
{ id: 4, name: "Milk", price: 5 },
];
const [cart, setCart] = useState([]);
const [view, setView] = useState<"products" | "cart" | "payment">("products");
// Load cart from localStorage
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
setCart(JSON.parse(saved));
}
}, []);
const saveCart = (updatedCart: CartItem[]) => {
setCart(updatedCart);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedCart));
};
const addToCart = (product: Product) => {
const existing = cart.find((item) => item.id === product.id);
let updatedCart: CartItem[];
if (existing) {
updatedCart = cart.map((item) =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
);
} else {
updatedCart = [...cart, { ...product, quantity: 1 }];
}
saveCart(updatedCart);
};
const handleClearCart = () => {
localStorage.removeItem(STORAGE_KEY);
setCart([]);
};
const getTotal = () =>
cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
Shopping Cart Example (Local Storage)
{/* Navigation */}
{/* Products View */}
{view === "products" && (
Products
{products.map((p) => (
-
{p.name} - ${p.price}{" "}
))}
)}
{/* Cart View */}
{view === "cart" && (
Your Cart
{cart.length === 0 ? (
No items in cart.
) : (
<>
{cart.map((item) => (
-
{item.name} x {item.quantity} = $
{item.price * item.quantity}
))}
Total: ${getTotal()}
>
)}
)}
{/* Payment View */}
{view === "payment" && (
Payment
{cart.length === 0 ? (
Your cart is empty.
) : (
)}
)}
);
}
export default App;
How Local Storage Works in This Example
Storing Data (addToCart / saveCart)
When a product is added or its quantity changes, the cart is updated and saved in Local Storage.
The localStorage.setItem(STORAGE_KEY, JSON.stringify(cart)) method is used for this.
Since Local Storage can only keep string values, the cart array is first converted into a JSON string.
Loading Data (useEffect on mount)
When the component first load, it check Local Storage for any saved cart data.
If data is found, it is retrieved with localStorage.getItem(STORAGE_KEY) and parsed back into an array with JSON.parse().
This ensures the cart persists even if the user refreshes the page.
Clearing Data (handleClearCart)
When the user clears the cart or completes payment, localStorage.removeItem(STORAGE_KEY) is called.
This deletes the saved cart data.
The React state for the cart is also reset to an empty array.
Persistence
Data in Local Storage persists even if the page reloads or browser restarts. Items in the cart stay there until the user remove them or completes payment.
This make it easy to add a “save draft” functionality in a React app with just a few lines of code.
With this setup, one can quickly implement a draft-saving logic in your React app using Local Storage with just a few lines of code.
Run the Example in CodeSandbox ↗
Option 2 : Save for Later with IndexedDB
What is IndexedDB?
IndexedDB is a browser-based, asynchronous database. Unlike Local Storage, it can handle larger data and allows more advanced queries.
- Size limit : Hundreds of MBs or more (depends on browser).
- Data type : Complex objects (not just strings).
- API : Promise-based (can use libraries like idb for simplicity).
Advantages of IndexedDB
- It can store large amounts of data (hundred of MBs).
- Supports complex objects (not just strings).
- Asynchronous → doesn’t block the UI.
- Supports queries, indexes, and transactions.
- Works offline → great for Progressive Web Apps (PWAs).
Disadvantages of IndexedDB
- More complex API compared to Local Storage.
- Requires async handling (Promises or callbacks).
- Not ideal for small, quick saves (overkill).
Limitations
- Different browser implementations may vary slightly.
- Debugging is harder than Local Storage.
- Requires a wrapper library (idb) for easier usage.
Where to Use IndexedDB
- E-commerce carts with multiple products.
- Offline-first applications (notes, task managers).
- Large forms or surveys with multiple steps.
- Storing media files (images, PDFs, videos).
Cleanup Strategy for IndexedDB
Over time, if many drafts are saved without being cleared, the database can grow very large. To prevent this, applications should implement a cleanup strategy. Some common approaches include :
- Auto-expiration : Store a timestamp with each entry and periodically delete drafts older than a certain age (e.g., 30 days).
- Limit the number of drafts : Keep only the last N drafts (e.g., the most recent 10), and delete older ones.
- Manual cleanup : Provide users with an option to clear saved drafts from the UI.
- Background cleanup : Run cleanup logic at app startup or at regular intervals to remove stale data.
This keeps IndexedDB storage small and lets users save things for later. IndexedDB can be tricky because saving or getting data takes a few steps. Libraries like idb, Dexie.js, or localForage make it easier. They work with promises and need less code, but you can still use all of IndexedDB’s features.
Full React Code Example
This code can be used in a React app—like in App.js for Create React App or Vite—to save, load, and clear form data using IndexedDB.
console.log( 'Code is Poetry' );// App.tsx
import React, { useState, useEffect } from "react";
const DB_NAME = "ShoppingCartDB";
const STORE_NAME = "cart";
const DB_VERSION = 1;
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem extends Product {
quantity: number;
}
// --- IndexedDB helpers ---
function openDB(): Promise {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: "id" });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function saveCartItem(item: CartItem) {
const db = await openDB();
const tx = db.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).put(item);
return tx.done;
}
async function loadCart(): Promise {
const db = await openDB();
return new Promise((resolve) => {
const tx = db.transaction(STORE_NAME, "readonly");
const req = tx.objectStore(STORE_NAME).getAll();
req.onsuccess = () => resolve((req.result as CartItem[]) || []);
});
}
async function clearCart() {
const db = await openDB();
const tx = db.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).clear();
return tx.done;
}
// --- Main App ---
function App() {
const products: Product[] = [
{ id: 1, name: "Apple", price: 2 },
{ id: 2, name: "Banana", price: 1 },
{ id: 3, name: "Orange", price: 3 },
{ id: 4, name: "Milk", price: 5 },
];
const [cart, setCart] = useState([]);
const [view, setView] = useState<"products" | "cart" | "payment">("products");
// Load cart from DB
useEffect(() => {
(async () => {
const savedCart = await loadCart();
setCart(savedCart);
})();
}, []);
const addToCart = async (product: Product) => {
const existing = cart.find((item) => item.id === product.id);
let updatedCart: CartItem[];
if (existing) {
updatedCart = cart.map((item) =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
);
} else {
updatedCart = [...cart, { ...product, quantity: 1 }];
}
setCart(updatedCart);
await saveCartItem(updatedCart.find((i) => i.id === product.id)!);
};
const handleClearCart = async () => {
await clearCart();
setCart([]);
};
const getTotal = () =>
cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
return (
Shopping Cart Example (IndexedDB)
{/* Navigation */}
{/* View: Products */}
{view === "products" && (
Products
{products.map((p) => (
-
{p.name} - ${p.price}{" "}
))}
)}
{/* View: Cart */}
{view === "cart" && (
Your Cart
{cart.length === 0 ? (
No items in cart.
) : (
<>
{cart.map((item) => (
-
{item.name} x {item.quantity} = $
{item.price * item.quantity}
))}
Total: ${getTotal()}
>
)}
)}
{/* View: Payment */}
{view === "payment" && (
Payment
{cart.length === 0 ? (
Your cart is empty.
) : (
)}
)}
);
}
export default App;
How IndexedDB Works in This Example
Database Setup (openDB)
indexedDB.open(DB_NAME, DB_VERSION) opens or creates a database called “ShoppingCartDB”.
In onupgradeneeded, an object store “cart” is created with a keyPath of “id”.
Each cart item is uniquely identified by its id, ensuring no duplicates.
Saving Data (saveCartItem)
Opens a readwrite transaction on the “cart” object store.
Uses put(item) to insert or update the cart item.
If item with the same id exists, it is updated; otherwise, a new entry is added.
Loading Data (loadCart)
Opens a readonly transaction on the “cart” store.
Retrieves all items using getAll().
Returns an array of CartItems. If the cart is empty, returns an empty array.
Clearing Data (clearCart)
Opens a readwrite transaction on the “cart” store.
Calls clear() to remove all cart entries.
Ensures the cart is empty after the operation.
React Integration
On page load, useEffect calls loadCart() to restore saved cart items from IndexedDB.
Adding an item to the cart :
Checks if the item already exists in the cart.
Updates quantity if it exists, otherwise adds a new CartItem.
Saves the new or updated item using saveCartItem.
Clearing the cart removes all items from IndexedDB and resets the state.
Payment form :
Uses <form onSubmit> with event.preventDefault() to prevent page reload.
After “payment”, clears the cart both in IndexedDB and React state.
Notes
Each cart item is identified by a unique id.
IndexedDB allows data persistence across browser sessions and page reloads.
This setup supports multiple cart items, quantity updates, and full cart clearing.
Dynamic operations like adding multiple items and updating quantities are handled efficiently without overwriting unrelated entries.
This setup allows the app to keep data across sessions, even after a page refresh or browser restart.
Run the Example in CodeSandbox ↗
Local Storage vs IndexedDB
Feature | Local Storage | IndexedDB |
Data Size | ~5MB | Hundreds of MBs |
Data Type | Strings only | Objects, Blobs, Files |
API Complexity | Simple (sync) | Complex (async) |
Performance | Fast for small data | Efficient for large data |
Security | Accessible via JS (XSS risk) | Slightly safer (structured) |
Best For | Drafts, preferences | Carts, offline storage |
Common Pitfalls
While Local Storage and IndexedDB are useful, they also come with some caveats developers should be aware of :
Browser Storage Reset
If the user clear browser data (cookies, cache, site storage), everything in Local Storage and IndexedDB will also be deleted. This means saved drafts or offline data may be lost.Incognito/Private Mode Restrictions
In incognito or private browsing modes, storage works differently. Local Storage and IndexedDB may have stricter limits, or in some browsers, they might not persist once the private session is closed.Storage Limits
Local Storage usually allows up to about 5MB of data. IndexedDB can hold much more, but the exact limit can change depending on the browser or device. Trying to store too much data might fail.Cross-Browser Differences
Browsers don’t always handle IndexedDB the same way. Some issues can appear, especially on older browsers or on mobile devices.Security Risks
Data in Local Storage can be read by JavaScript, so any XSS bug could expose it. IndexedDB is safer for storing structured data, but sensitive information should still be kept out.
Conclusion
A “Save for Later” feature makes life easier for users. They don’t lose what they were doing and can come back later. Local Storage works for small stuff, like a quick draft, while IndexedDB is better for bigger or more complicated data. Picking the right one just makes your app work smoother.

