useHooks.iov4.1.2
DocsBlogGitHub

Mastering Client-Side Storage: The useIndexedDB Hook

Unlock the power of IndexedDB in React with the useIndexedDB hook - featuring automatic database management, transaction handling, and seamless state synchronization for large-scale data storage.

By usehooks.io

use-indexed-dbstatestorageindexeddbdatabase

When localStorage isn't enough for your application's data storage needs, IndexedDB provides a powerful solution for storing large amounts of structured data in the browser. The useIndexedDB hook brings the full power of IndexedDB to React applications with an intuitive, promise-based API that handles all the complexity of database management.

What is useIndexedDB?

The useIndexedDB hook is a comprehensive React hook that provides a high-level interface to IndexedDB, the browser's built-in NoSQL database. Unlike localStorage, which is limited to strings and has size constraints, IndexedDB can store complex objects, files, and large datasets with powerful querying capabilities.

Key Features

🗄️ Powerful Storage

Store complex objects, files, blobs, and large datasets without the limitations of localStorage or sessionStorage.

🔄 Automatic Database Management

Handles database initialization, schema upgrades, and connection management automatically.

🛡️ Transaction Safety

All operations are wrapped in IndexedDB transactions, ensuring data consistency and integrity.

⚡ Asynchronous Operations

Promise-based API that integrates seamlessly with modern async/await patterns.

🎯 Type Safe

Fully typed with TypeScript generics, providing complete type safety for your stored data.

🔧 Schema Upgrades

Supports database versioning and custom upgrade handlers for evolving data structures.

The Implementation

Let's examine the core functionality of this hook:

1"use client";
2
3import { useState, useEffect, useCallback } from "react";
4
5interface UseIndexedDBOptions {
6  version?: number;
7  onUpgradeNeeded?: (
8    db: IDBDatabase,
9    oldVersion: number,
10    newVersion: number
11  ) => void;
12}
13
14interface UseIndexedDBReturn<T> {
15  data: T | null;
16  error: string | null;
17  loading: boolean;
18  setItem: (key: string, value: T) => Promise<void>;
19  getItem: (key: string) => Promise<T | null>;
20  removeItem: (key: string) => Promise<void>;
21  clear: () => Promise<void>;
22  getAllKeys: () => Promise<string[]>;
23}
24
25export function useIndexedDB<T = any>(
26  databaseName: string,
27  storeName: string,
28  options: UseIndexedDBOptions = {}
29): UseIndexedDBReturn<T> {
30  // Implementation details...
31}
32

Basic Usage

Let's break down the core functionality of the useIndexedDB hook:

Hook Interface

The hook accepts three parameters:

  • databaseName: Name of your IndexedDB database
  • storeName: Name of the object store to use
  • options: Optional configuration object for versioning and upgrades
1import { useIndexedDB } from "@usehooks-io/hooks";
2
3function UserDataManager() {
4  const { data, error, loading, setItem, getItem } = useIndexedDB(
5    "myApp",
6    "userData"
7  );
8
9  const saveUserData = async () => {
10    try {
11      await setItem("user-123", {
12        name: "John Doe",
13        email: "john@example.com",
14        preferences: {
15          theme: "dark",
16          notifications: true,
17        },
18      });
19      console.log("User data saved!");
20    } catch (err) {
21      console.error("Failed to save:", err);
22    }
23  };
24
25  const loadUserData = async () => {
26    try {
27      const userData = await getItem("user-123");
28      console.log("Loaded user:", userData);
29    } catch (err) {
30      console.error("Failed to load:", err);
31    }
32  };
33
34  if (loading) return <div>Initializing database...</div>;
35  if (error) return <div>Error: {error}</div>;
36
37  return (
38    <div>
39      <button onClick={saveUserData}>Save User Data</button>
40      <button onClick={loadUserData}>Load User Data</button>
41      {data && (
42        <div>
43          <h3>Current Data:</h3>
44          <pre>{JSON.stringify(data, null, 2)}</pre>
45        </div>
46      )}
47    </div>
48  );
49}
50

Advanced Usage with Custom Schema

For complex applications, you can define custom database schemas and handle upgrades:

1import { useIndexedDB } from "@usehooks-io/hooks";
2
3interface TodoItem {
4  id: string;
5  title: string;
6  description: string;
7  completed: boolean;
8  priority: "low" | "medium" | "high";
9  createdAt: Date;
10  updatedAt: Date;
11  tags: string[];
12}
13
14function TodoManager() {
15  const { setItem, getItem, getAllKeys, removeItem, clear, loading, error } =
16    useIndexedDB<TodoItem>("todoApp", "todos", {
17      version: 2,
18      onUpgradeNeeded: (db, oldVersion, newVersion) => {
19        console.log(`Upgrading from ${oldVersion} to ${newVersion}`);
20
21        if (oldVersion < 1) {
22          // Create todos store
23          const todosStore = db.createObjectStore("todos");
24          console.log("Created todos object store");
25        }
26
27        if (oldVersion < 2) {
28          // Add indexes for better querying
29          const transaction = db.transaction(["todos"], "readwrite");
30          const todosStore = transaction.objectStore("todos");
31
32          if (!todosStore.indexNames.contains("completed")) {
33            todosStore.createIndex("completed", "completed", { unique: false });
34          }
35
36          if (!todosStore.indexNames.contains("priority")) {
37            todosStore.createIndex("priority", "priority", { unique: false });
38          }
39
40          if (!todosStore.indexNames.contains("createdAt")) {
41            todosStore.createIndex("createdAt", "createdAt", { unique: false });
42          }
43
44          console.log("Added indexes for todos");
45        }
46      },
47    });
48
49  const [todos, setTodos] = useState<TodoItem[]>([]);
50
51  // Load all todos on component mount
52  useEffect(() => {
53    const loadAllTodos = async () => {
54      try {
55        const keys = await getAllKeys();
56        const todoPromises = keys.map((key) => getItem(key));
57        const loadedTodos = await Promise.all(todoPromises);
58        setTodos(loadedTodos.filter(Boolean) as TodoItem[]);
59      } catch (err) {
60        console.error("Failed to load todos:", err);
61      }
62    };
63
64    if (!loading) {
65      loadAllTodos();
66    }
67  }, [loading, getAllKeys, getItem]);
68
69  const addTodo = async (title: string, description: string) => {
70    const todo: TodoItem = {
71      id: crypto.randomUUID(),
72      title,
73      description,
74      completed: false,
75      priority: "medium",
76      createdAt: new Date(),
77      updatedAt: new Date(),
78      tags: [],
79    };
80
81    try {
82      await setItem(todo.id, todo);
83      setTodos((prev) => [...prev, todo]);
84    } catch (err) {
85      console.error("Failed to add todo:", err);
86    }
87  };
88
89  const updateTodo = async (id: string, updates: Partial<TodoItem>) => {
90    try {
91      const existingTodo = await getItem(id);
92      if (existingTodo) {
93        const updatedTodo = {
94          ...existingTodo,
95          ...updates,
96          updatedAt: new Date(),
97        };
98        await setItem(id, updatedTodo);
99        setTodos((prev) =>
100          prev.map((todo) => (todo.id === id ? updatedTodo : todo))
101        );
102      }
103    } catch (err) {
104      console.error("Failed to update todo:", err);
105    }
106  };
107
108  const deleteTodo = async (id: string) => {
109    try {
110      await removeItem(id);
111      setTodos((prev) => prev.filter((todo) => todo.id !== id));
112    } catch (err) {
113      console.error("Failed to delete todo:", err);
114    }
115  };
116
117  const clearAllTodos = async () => {
118    try {
119      await clear();
120      setTodos([]);
121    } catch (err) {
122      console.error("Failed to clear todos:", err);
123    }
124  };
125
126  if (loading) return <div>Initializing todo database...</div>;
127  if (error) return <div>Database error: {error}</div>;
128
129  return (
130    <div>
131      <h2>Todo Manager</h2>
132
133      <div>
134        <button
135          onClick={() => addTodo("Sample Todo", "This is a sample todo item")}
136        >
137          Add Sample Todo
138        </button>
139        <button onClick={clearAllTodos} style={{ marginLeft: "10px" }}>
140          Clear All Todos
141        </button>
142      </div>
143
144      <div>
145        <h3>Todos ({todos.length})</h3>
146        {todos.map((todo) => (
147          <div
148            key={todo.id}
149            style={{
150              border: "1px solid #ccc",
151              margin: "10px",
152              padding: "10px",
153            }}
154          >
155            <h4>{todo.title}</h4>
156            <p>{todo.description}</p>
157            <p>Priority: {todo.priority}</p>
158            <p>Status: {todo.completed ? "Completed" : "Pending"}</p>
159            <p>Created: {todo.createdAt.toLocaleDateString()}</p>
160
161            <button
162              onClick={() =>
163                updateTodo(todo.id, { completed: !todo.completed })
164              }
165            >
166              {todo.completed ? "Mark Incomplete" : "Mark Complete"}
167            </button>
168
169            <button
170              onClick={() => deleteTodo(todo.id)}
171              style={{ marginLeft: "10px" }}
172            >
173              Delete
174            </button>
175          </div>
176        ))}
177      </div>
178    </div>
179  );
180}
181

File Storage Example

IndexedDB excels at storing files and binary data:

1import { useIndexedDB } from "@usehooks-io/hooks";
2
3interface FileMetadata {
4  name: string;
5  size: number;
6  type: string;
7  uploadedAt: Date;
8  file: File;
9}
10
11function FileManager() {
12  const { setItem, getItem, getAllKeys, removeItem, loading, error } =
13    useIndexedDB<FileMetadata>("fileStorage", "files");
14
15  const [files, setFiles] = useState<FileMetadata[]>([]);
16
17  // Load all files on mount
18  useEffect(() => {
19    const loadFiles = async () => {
20      try {
21        const keys = await getAllKeys();
22        const filePromises = keys.map((key) => getItem(key));
23        const loadedFiles = await Promise.all(filePromises);
24        setFiles(loadedFiles.filter(Boolean) as FileMetadata[]);
25      } catch (err) {
26        console.error("Failed to load files:", err);
27      }
28    };
29
30    if (!loading) {
31      loadFiles();
32    }
33  }, [loading, getAllKeys, getItem]);
34
35  const handleFileUpload = async (
36    event: React.ChangeEvent<HTMLInputElement>
37  ) => {
38    const uploadedFiles = event.target.files;
39    if (!uploadedFiles) return;
40
41    for (const file of Array.from(uploadedFiles)) {
42      try {
43        const fileMetadata: FileMetadata = {
44          name: file.name,
45          size: file.size,
46          type: file.type,
47          uploadedAt: new Date(),
48          file,
49        };
50
51        await setItem(file.name, fileMetadata);
52        setFiles((prev) => [...prev, fileMetadata]);
53        console.log(`File ${file.name} stored successfully!`);
54      } catch (err) {
55        console.error(`Failed to store file ${file.name}:`, err);
56      }
57    }
58  };
59
60  const downloadFile = async (fileName: string) => {
61    try {
62      const fileMetadata = await getItem(fileName);
63      if (fileMetadata) {
64        const url = URL.createObjectURL(fileMetadata.file);
65        const a = document.createElement("a");
66        a.href = url;
67        a.download = fileName;
68        document.body.appendChild(a);
69        a.click();
70        document.body.removeChild(a);
71        URL.revokeObjectURL(url);
72      }
73    } catch (err) {
74      console.error("Failed to download file:", err);
75    }
76  };
77
78  const deleteFile = async (fileName: string) => {
79    try {
80      await removeItem(fileName);
81      setFiles((prev) => prev.filter((file) => file.name !== fileName));
82    } catch (err) {
83      console.error("Failed to delete file:", err);
84    }
85  };
86
87  const formatFileSize = (bytes: number) => {
88    if (bytes === 0) return "0 Bytes";
89    const k = 1024;
90    const sizes = ["Bytes", "KB", "MB", "GB"];
91    const i = Math.floor(Math.log(bytes) / Math.log(k));
92    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
93  };
94
95  if (loading) return <div>Initializing file storage...</div>;
96  if (error) return <div>Storage error: {error}</div>;
97
98  return (
99    <div>
100      <h2>File Manager</h2>
101
102      <div>
103        <input
104          type="file"
105          multiple
106          onChange={handleFileUpload}
107          style={{ marginBottom: "20px" }}
108        />
109      </div>
110
111      <div>
112        <h3>Stored Files ({files.length})</h3>
113        {files.length === 0 ? (
114          <p>No files stored yet. Upload some files to get started!</p>
115        ) : (
116          <div>
117            {files.map((fileMetadata) => (
118              <div
119                key={fileMetadata.name}
120                style={{
121                  border: "1px solid #ddd",
122                  borderRadius: "4px",
123                  padding: "10px",
124                  margin: "10px 0",
125                  display: "flex",
126                  justifyContent: "space-between",
127                  alignItems: "center",
128                }}
129              >
130                <div>
131                  <strong>{fileMetadata.name}</strong>
132                  <br />
133                  <small>
134                    {fileMetadata.type}{formatFileSize(fileMetadata.size)}{" "}
135                    Uploaded {fileMetadata.uploadedAt.toLocaleDateString()}
136                  </small>
137                </div>
138                <div>
139                  <button
140                    onClick={() => downloadFile(fileMetadata.name)}
141                    style={{ marginRight: "10px" }}
142                  >
143                    Download
144                  </button>
145                  <button
146                    onClick={() => deleteFile(fileMetadata.name)}
147                    style={{ backgroundColor: "#ff4444", color: "white" }}
148                  >
149                    Delete
150                  </button>
151                </div>
152              </div>
153            ))}
154          </div>
155        )}
156      </div>
157    </div>
158  );
159}
160

Offline Data Synchronization

IndexedDB is perfect for building offline-capable applications:

1import { useIndexedDB } from "@usehooks-io/hooks";
2
3interface SyncableData {
4  id: string;
5  content: any;
6  lastModified: Date;
7  synced: boolean;
8  action: "create" | "update" | "delete";
9}
10
11function OfflineDataManager() {
12  const { setItem, getItem, getAllKeys, removeItem, loading, error } =
13    useIndexedDB<SyncableData>("offlineApp", "pendingSync");
14
15  const [pendingItems, setPendingItems] = useState<SyncableData[]>([]);
16  const [isOnline, setIsOnline] = useState(navigator.onLine);
17
18  // Monitor online status
19  useEffect(() => {
20    const handleOnline = () => setIsOnline(true);
21    const handleOffline = () => setIsOnline(false);
22
23    window.addEventListener("online", handleOnline);
24    window.addEventListener("offline", handleOffline);
25
26    return () => {
27      window.removeEventListener("online", handleOnline);
28      window.removeEventListener("offline", handleOffline);
29    };
30  }, []);
31
32  // Load pending sync items
33  useEffect(() => {
34    const loadPendingItems = async () => {
35      try {
36        const keys = await getAllKeys();
37        const itemPromises = keys.map((key) => getItem(key));
38        const items = await Promise.all(itemPromises);
39        setPendingItems(items.filter(Boolean) as SyncableData[]);
40      } catch (err) {
41        console.error("Failed to load pending items:", err);
42      }
43    };
44
45    if (!loading) {
46      loadPendingItems();
47    }
48  }, [loading, getAllKeys, getItem]);
49
50  // Auto-sync when online
51  useEffect(() => {
52    if (isOnline && pendingItems.length > 0) {
53      syncPendingItems();
54    }
55  }, [isOnline, pendingItems.length]);
56
57  const addPendingItem = async (
58    id: string,
59    content: any,
60    action: "create" | "update" | "delete"
61  ) => {
62    const item: SyncableData = {
63      id,
64      content,
65      lastModified: new Date(),
66      synced: false,
67      action,
68    };
69
70    try {
71      await setItem(id, item);
72      setPendingItems((prev) => {
73        const existing = prev.find((p) => p.id === id);
74        if (existing) {
75          return prev.map((p) => (p.id === id ? item : p));
76        }
77        return [...prev, item];
78      });
79    } catch (err) {
80      console.error("Failed to add pending item:", err);
81    }
82  };
83
84  const syncPendingItems = async () => {
85    console.log("Starting sync...");
86
87    for (const item of pendingItems) {
88      try {
89        // Simulate API call
90        await new Promise((resolve) => setTimeout(resolve, 1000));
91
92        // Mark as synced and remove from IndexedDB
93        await removeItem(item.id);
94        setPendingItems((prev) => prev.filter((p) => p.id !== item.id));
95
96        console.log(`Synced item ${item.id}`);
97      } catch (err) {
98        console.error(`Failed to sync item ${item.id}:`, err);
99        break; // Stop syncing on error
100      }
101    }
102
103    console.log("Sync completed");
104  };
105
106  const createItem = async (content: any) => {
107    const id = crypto.randomUUID();
108    await addPendingItem(id, content, "create");
109  };
110
111  const updateItem = async (id: string, content: any) => {
112    await addPendingItem(id, content, "update");
113  };
114
115  const deleteItem = async (id: string) => {
116    await addPendingItem(id, null, "delete");
117  };
118
119  if (loading) return <div>Initializing offline storage...</div>;
120  if (error) return <div>Storage error: {error}</div>;
121
122  return (
123    <div>
124      <h2>Offline Data Manager</h2>
125
126      <div style={{ marginBottom: "20px" }}>
127        <p>
128          Status: {isOnline ? "🟢 Online" : "🔴 Offline"} • Pending sync:{" "}
129          {pendingItems.length} items
130        </p>
131
132        {isOnline && pendingItems.length > 0 && (
133          <button onClick={syncPendingItems}>Sync Now</button>
134        )}
135      </div>
136
137      <div>
138        <button
139          onClick={() => createItem({ title: "New Item", data: Math.random() })}
140        >
141          Create Item
142        </button>
143        <button
144          onClick={() =>
145            updateItem("item-1", { title: "Updated Item", data: Math.random() })
146          }
147          style={{ marginLeft: "10px" }}
148        >
149          Update Item
150        </button>
151        <button
152          onClick={() => deleteItem("item-2")}
153          style={{ marginLeft: "10px" }}
154        >
155          Delete Item
156        </button>
157      </div>
158
159      <div>
160        <h3>Pending Sync Items</h3>
161        {pendingItems.length === 0 ? (
162          <p>No pending items</p>
163        ) : (
164          pendingItems.map((item) => (
165            <div
166              key={item.id}
167              style={{
168                border: "1px solid #ccc",
169                padding: "10px",
170                margin: "5px",
171              }}
172            >
173              <strong>ID:</strong> {item.id}
174              <br />
175              <strong>Action:</strong> {item.action}
176              <br />
177              <strong>Modified:</strong> {item.lastModified.toLocaleString()}
178              <br />
179              <strong>Content:</strong> {JSON.stringify(item.content)}
180            </div>
181          ))
182        )}
183      </div>
184    </div>
185  );
186}
187

Real-World Applications

The useIndexedDB hook is perfect for applications that need robust client-side storage:

  1. Offline-First Apps: Store data locally and sync when online
  2. File Management: Upload, store, and manage files in the browser
  3. Complex Data Structures: Store nested objects, arrays, and relationships
  4. Large Datasets: Handle thousands of records with efficient querying
  5. Media Applications: Store images, videos, and audio files
  6. Progressive Web Apps: Enable full offline functionality
  7. Data Caching: Cache API responses for improved performance
  8. Draft Management: Auto-save complex forms and documents

IndexedDB vs localStorage vs sessionStorage

FeatureIndexedDBlocalStoragesessionStorage
Storage Limit~50MB-1GB+~5-10MB~5-10MB
Data TypesAny (objects, files, blobs)Strings onlyStrings only
PersistencePermanentPermanentSession only
Transactions✅ Yes❌ No❌ No
Indexing✅ Yes❌ No❌ No
Async API✅ Yes❌ No❌ No
Complex Queries✅ Yes❌ No❌ No
Schema Evolution✅ Yes❌ No❌ No

Best Practices

  1. Design Your Schema: Plan your object stores and indexes before implementation

  2. Handle Upgrades Gracefully: Use the onUpgradeNeeded callback for schema migrations

  3. Error Handling: Always wrap IndexedDB operations in try-catch blocks

  4. Performance: Use indexes for frequently queried fields

  5. Storage Quotas: Monitor storage usage and handle quota exceeded errors

  6. Cleanup: Implement data cleanup strategies for old or unused data

  7. Testing: Test in various browsers and private browsing modes

  8. Backup Strategy: Consider server-side backups for critical data

Browser Compatibility

IndexedDB is supported in all modern browsers:

  • Chrome 24+
  • Firefox 16+
  • Safari 10+
  • Edge 12+
  • iOS Safari 10+
  • Android Browser 4.4+

The hook gracefully handles environments where IndexedDB is unavailable.

Conclusion

The useIndexedDB hook transforms IndexedDB from a complex, low-level API into an intuitive, React-friendly interface. Its automatic database management, transaction handling, and promise-based operations make it the perfect choice for applications requiring robust client-side storage.

Whether you're building offline-capable applications, managing large datasets, or storing complex media files, useIndexedDB provides the power and flexibility you need while maintaining the simplicity that React developers expect.

Start leveraging the full potential of browser storage with useIndexedDB and unlock new possibilities for your React applications!