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
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 databasestoreName
: Name of the object store to useoptions
: 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:
- Offline-First Apps: Store data locally and sync when online
- File Management: Upload, store, and manage files in the browser
- Complex Data Structures: Store nested objects, arrays, and relationships
- Large Datasets: Handle thousands of records with efficient querying
- Media Applications: Store images, videos, and audio files
- Progressive Web Apps: Enable full offline functionality
- Data Caching: Cache API responses for improved performance
- Draft Management: Auto-save complex forms and documents
IndexedDB vs localStorage vs sessionStorage
Feature | IndexedDB | localStorage | sessionStorage |
---|---|---|---|
Storage Limit | ~50MB-1GB+ | ~5-10MB | ~5-10MB |
Data Types | Any (objects, files, blobs) | Strings only | Strings only |
Persistence | Permanent | Permanent | Session 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
-
Design Your Schema: Plan your object stores and indexes before implementation
-
Handle Upgrades Gracefully: Use the
onUpgradeNeeded
callback for schema migrations -
Error Handling: Always wrap IndexedDB operations in try-catch blocks
-
Performance: Use indexes for frequently queried fields
-
Storage Quotas: Monitor storage usage and handle quota exceeded errors
-
Cleanup: Implement data cleanup strategies for old or unused data
-
Testing: Test in various browsers and private browsing modes
-
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!