ReactJS

Add Save for Later Functionality with Local Storage/IndexedDB

Avinash Prajapati
Avinash PrajapatiNov 24, 2025

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.

1// App.tsx
2import React, { useState, useEffect } from "react";
3
4interface Product {
5  id: number;
6  name: string;
7  price: number;
8}
9
10interface CartItem extends Product {
11  quantity: number;
12}
13
14const STORAGE_KEY = "shopping_cart";
15
16function App() {
17  const products: Product[] = [
18    { id: 1, name: "Apple", price: 2 },
19    { id: 2, name: "Banana", price: 1 },
20    { id: 3, name: "Orange", price: 3 },
21    { id: 4, name: "Milk", price: 5 },
22  ];
23
24  const [cart, setCart] = useState<CartItem[]>([]);
25  const [view, setView] = useState<"products" | "cart" | "payment">("products");
26
27  // Load cart from localStorage
28  useEffect(() => {
29    const saved = localStorage.getItem(STORAGE_KEY);
30    if (saved) {
31      setCart(JSON.parse(saved));
32    }
33  }, []);
34
35  const saveCart = (updatedCart: CartItem[]) => {
36    setCart(updatedCart);
37    localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedCart));
38  };
39
40  const addToCart = (product: Product) => {
41    const existing = cart.find((item) => item.id === product.id);
42    let updatedCart: CartItem[];
43
44    if (existing) {
45      updatedCart = cart.map((item) =>
46        item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
47      );
48    } else {
49      updatedCart = [...cart, { ...product, quantity: 1 }];
50    }
51
52    saveCart(updatedCart);
53  };
54
55  const handleClearCart = () => {
56    localStorage.removeItem(STORAGE_KEY);
57    setCart([]);
58  };
59
60  const getTotal = () =>
61    cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
62
63  return (
64    <div style={{ padding: "20px" }}>
65      <h2>Shopping Cart Example (Local Storage)</h2>
66
67      {/* Navigation */}
68      <nav>
69        <button style={{ marginRight: '4px' }} onClick={() => setView("products")}>Products</button>
70        <button style={{ marginRight: '4px' }} onClick={() => setView("cart")}>
71          Cart ({cart.reduce((sum, i) => sum + i.quantity, 0)})
72        </button>
73        <button onClick={() => setView("payment")}>Payment</button>
74      </nav>
75
76      <hr />
77
78      {/* Products View */}
79      {view === "products" && (
80        <div>
81          <h3>Products</h3>
82          <ul>
83            {products.map((p) => (
84              <li key={p.id}>
85                {p.name} - ${p.price}{" "}
86                <button onClick={() => addToCart(p)}>Add to Cart</button>
87              </li>
88            ))}
89          </ul>
90        </div>
91      )}
92
93      {/* Cart View */}
94      {view === "cart" && (
95        <div>
96          <h3>Your Cart</h3>
97          {cart.length === 0 ? (
98            <p>No items in cart.</p>
99          ) : (
100            <>
101              <ul>
102                {cart.map((item) => (
103                  <li key={item.id}>
104                    {item.name} x {item.quantity} = $
105                    {item.price * item.quantity}
106                  </li>
107                ))}
108              </ul>
109              <p>
110                <strong>Total:</strong> ${getTotal()}
111              </p>
112              <button onClick={handleClearCart}>Clear Cart</button>
113            </>
114          )}
115        </div>
116      )}
117
118      {/* Payment View */}
119      {view === "payment" && (
120        <div>
121          <h3>Payment</h3>
122          {cart.length === 0 ? (
123            <p>Your cart is empty.</p>
124          ) : (
125            <form
126              onSubmit={(e) => {
127                e.preventDefault(); // prevent reload
128                alert("Payment successful!");
129                handleClearCart();
130              }}
131            >
132              <input style={{ marginBottom: '4px' }} type="text" placeholder="Card Number" required />
133              <br />
134              <input style={{ marginBottom: '4px' }} type="text" placeholder="Address" required />
135              <br />
136              <button type="submit">Pay ${getTotal()}</button>
137            </form>
138          )}
139        </div>
140      )}
141    </div>
142  );
143}
144
145export 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.

1// App.tsx
2import React, { useState, useEffect } from "react";
3
4const DB_NAME = "ShoppingCartDB";
5const STORE_NAME = "cart";
6const DB_VERSION = 1;
7
8interface Product {
9  id: number;
10  name: string;
11  price: number;
12}
13
14interface CartItem extends Product {
15  quantity: number;
16}
17
18// --- IndexedDB helpers ---
19function openDB(): Promise<IDBDatabase> {
20  return new Promise((resolve, reject) => {
21    const request = indexedDB.open(DB_NAME, DB_VERSION);
22
23    request.onupgradeneeded = () => {
24      const db = request.result;
25      if (!db.objectStoreNames.contains(STORE_NAME)) {
26        db.createObjectStore(STORE_NAME, { keyPath: "id" });
27      }
28    };
29
30    request.onsuccess = () => resolve(request.result);
31    request.onerror = () => reject(request.error);
32  });
33}
34
35async function saveCartItem(item: CartItem) {
36  const db = await openDB();
37  const tx = db.transaction(STORE_NAME, "readwrite");
38  tx.objectStore(STORE_NAME).put(item);
39  return tx.done;
40}
41
42async function loadCart(): Promise<CartItem[]> {
43  const db = await openDB();
44  return new Promise((resolve) => {
45    const tx = db.transaction(STORE_NAME, "readonly");
46    const req = tx.objectStore(STORE_NAME).getAll();
47    req.onsuccess = () => resolve((req.result as CartItem[]) || []);
48  });
49}
50
51async function clearCart() {
52  const db = await openDB();
53  const tx = db.transaction(STORE_NAME, "readwrite");
54  tx.objectStore(STORE_NAME).clear();
55  return tx.done;
56}
57
58// --- Main App ---
59function App() {
60  const products: Product[] = [
61    { id: 1, name: "Apple", price: 2 },
62    { id: 2, name: "Banana", price: 1 },
63    { id: 3, name: "Orange", price: 3 },
64    { id: 4, name: "Milk", price: 5 },
65  ];
66
67  const [cart, setCart] = useState<CartItem[]>([]);
68  const [view, setView] = useState<"products" | "cart" | "payment">("products");
69
70  // Load cart from DB
71  useEffect(() => {
72    (async () => {
73      const savedCart = await loadCart();
74      setCart(savedCart);
75    })();
76  }, []);
77
78  const addToCart = async (product: Product) => {
79    const existing = cart.find((item) => item.id === product.id);
80    let updatedCart: CartItem[];
81
82    if (existing) {
83      updatedCart = cart.map((item) =>
84        item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
85      );
86    } else {
87      updatedCart = [...cart, { ...product, quantity: 1 }];
88    }
89
90    setCart(updatedCart);
91    await saveCartItem(updatedCart.find((i) => i.id === product.id)!);
92  };
93
94  const handleClearCart = async () => {
95    await clearCart();
96    setCart([]);
97  };
98
99  const getTotal = () =>
100    cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
101
102  return (
103    <div style={{ padding: "20px" }}>
104      <h2>Shopping Cart Example (IndexedDB)</h2>
105
106      {/* Navigation */}
107      <nav>
108        <button style={{marginRight: '4px'}} onClick={() => setView("products")}>Products</button>
109        <button style={{marginRight: '4px'}} onClick={() => setView("cart")}>
110          Cart ({cart.reduce((sum, i) => sum + i.quantity, 0)})
111        </button>
112        <button onClick={() => setView("payment")}>Payment</button>
113      </nav>
114
115      <hr />
116
117      {/* View: Products */}
118      {view === "products" && (
119        <div>
120          <h3>Products</h3>
121          <ul>
122            {products.map((p) => (
123              <li key={p.id}>
124                {p.name} - ${p.price}{" "}
125                <button onClick={() => addToCart(p)}>Add to Cart</button>
126              </li>
127            ))}
128          </ul>
129        </div>
130      )}
131
132      {/* View: Cart */}
133      {view === "cart" && (
134        <div>
135          <h3>Your Cart</h3>
136          {cart.length === 0 ? (
137            <p>No items in cart.</p>
138          ) : (
139            <>
140              <ul>
141                {cart.map((item) => (
142                  <li key={item.id}>
143                    {item.name} x {item.quantity} = $
144                    {item.price * item.quantity}
145                  </li>
146                ))}
147              </ul>
148              <p>
149                <strong>Total:</strong> ${getTotal()}
150              </p>
151              <button onClick={handleClearCart}>Clear Cart</button>
152            </>
153          )}
154        </div>
155      )}
156
157      {/* View: Payment */}
158      {view === "payment" && (
159        <div>
160          <h3>Payment</h3>
161          {cart.length === 0 ? (
162            <p>Your cart is empty.</p>
163          ) : (
164            <form
165              onSubmit={(e) => {
166                e.preventDefault(); // prevent page reload
167                alert("Payment successful!");
168                handleClearCart();
169              }}
170            >
171              <input style={{ marginBottom: '4px' }} type="text" placeholder="Card Number" required />
172              <br />
173              <input style={{ marginBottom: '4px' }} type="text" placeholder="Address" required />
174              <br />
175              <button type="submit">Pay ${getTotal()}</button>
176            </form>
177          )}
178        </div>
179      )}
180    </div>
181  );
182}
183
184export 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

FeatureLocal StorageIndexedDB
Data Size~5MBHundreds of MBs
Data TypeStrings onlyObjects, Blobs, Files
API ComplexitySimple (sync)Complex (async)
PerformanceFast for small dataEfficient for large data
SecurityAccessible via JS (XSS risk)Slightly safer (structured)
Best ForDrafts, preferencesCarts, 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.



© 2026 IGNEK. All rights reserved.

Ignek on LinkedInIgnek on InstagramIgnek on FacebookIgnek on YouTubeIgnek on X