useSuspenseIndexedDBState
Reads and writes IndexedDB state with Suspense support.
About
A Suspense-enabled hook for IndexedDB state management with cross-tab synchronization. This hook suspends during initialization and must be wrapped in a Suspense boundary. It provides full TypeScript generic support, structured data storage without JSON serialization limits, error handling, and automatic synchronization across browser tabs using BroadcastChannel API.
Examples
Basic usage with simple values
import React, { Suspense } from "react";
import { useSuspenseIndexedDBState } from "rooks/experimental";
function Counter() {
const [count, { setItem, deleteItem }] = useSuspenseIndexedDBState(
"my-counter",
(currentValue) => (typeof currentValue === "number" ? currentValue : 0)
);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setItem(count + 1)}>Increment</button>
<button onClick={deleteItem}>Reset</button>
</div>
);
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Counter />
</Suspense>
);
}Working with Complex Objects and TypeScript
import React, { Suspense } from "react";
import { useSuspenseIndexedDBState } from "rooks/experimental";
interface UserProfile {
id: number;
name: string;
email: string;
preferences: {
theme: "light" | "dark";
notifications: boolean;
};
createdAt: Date;
}
function UserProfileManager() {
const [profile, { setItem, getItem, deleteItem }] = useSuspenseIndexedDBState(
"user-profile",
(currentValue): UserProfile => {
if (
currentValue &&
typeof currentValue === "object" &&
"id" in currentValue
) {
// IndexedDB can store Date objects directly, no JSON parsing needed
return currentValue as UserProfile;
}
return {
id: 0,
name: "Guest",
email: "",
preferences: { theme: "light", notifications: true },
createdAt: new Date(),
};
}
);
const updateProfile = () => {
setItem({
id: 123,
name: "John Doe",
email: "john@example.com",
preferences: { theme: "dark", notifications: false },
createdAt: new Date(), // Date objects work directly with IndexedDB
});
};
const getCurrentProfile = () => {
const current = getItem();
console.log("Current profile:", current);
};
return (
<div>
<h2>User Profile</h2>
<p>Name: {profile.name}</p>
<p>Email: {profile.email}</p>
<p>Theme: {profile.preferences.theme}</p>
<p>Created: {profile.createdAt.toLocaleDateString()}</p>
<button onClick={updateProfile}>Update Profile</button>
<button onClick={getCurrentProfile}>Log Current Profile</button>
<button onClick={deleteItem}>Reset Profile</button>
</div>
);
}Custom Database Configuration
import React, { Suspense } from "react";
import { useSuspenseIndexedDBState } from "rooks/experimental";
type ItemData = { items: string[] };
function CustomDBExample() {
const [data, { setItem }] = useSuspenseIndexedDBState<ItemData>(
"my-data",
(currentValue) =>
currentValue &&
typeof currentValue === "object" &&
"items" in currentValue
? (currentValue as ItemData)
: { items: [] },
{
dbName: "my-app-db",
storeName: "app-data",
version: 2,
}
);
return (
<div>
<p>Items: {data.items.length}</p>
<button
onClick={() =>
setItem({
items: [...data.items, `Item ${data.items.length + 1}`],
})
}
>
Add Item
</button>
</div>
);
}Storing Binary Data and Complex Structures
import React, { Suspense } from "react";
import { useSuspenseIndexedDBState } from "rooks/experimental";
interface AppData {
settings: Map<string, any>; // Maps work with IndexedDB
files: File[]; // File objects work with IndexedDB
metadata: {
version: string;
lastSync: Date;
tags: Set<string>; // Sets work with IndexedDB
};
}
function ComplexDataExample() {
const [appData, { setItem }] = useSuspenseIndexedDBState(
"complex-app-data",
(currentValue): AppData => {
if (currentValue) {
return currentValue as AppData;
}
const settings = new Map();
settings.set("theme", "light");
settings.set("autoSave", true);
return {
settings,
files: [],
metadata: {
version: "1.0.0",
lastSync: new Date(),
tags: new Set(["important", "project"]),
},
};
}
);
const addFile = (file: File) => {
setItem({
...appData,
files: [...appData.files, file],
metadata: {
...appData.metadata,
lastSync: new Date(),
},
});
};
return (
<div>
<h2>Complex Data Storage</h2>
<p>Settings: {appData.settings.size} items</p>
<p>Files: {appData.files.length}</p>
<p>Tags: {Array.from(appData.metadata.tags).join(", ")}</p>
<p>Last Sync: {appData.metadata.lastSync.toLocaleString()}</p>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) addFile(file);
}}
/>
</div>
);
}Cross-tab Synchronization
The hook automatically synchronizes state changes across browser tabs using the BroadcastChannel API:
import React, { Suspense } from "react";
import { useSuspenseIndexedDBState } from "rooks/experimental";
function SyncDemo() {
const [message, { setItem, deleteItem }] = useSuspenseIndexedDBState<string>(
"sync-message",
(currentValue) =>
typeof currentValue === "string" ? currentValue : "Hello World"
);
const [lastUpdated, { setItem: setLastUpdated }] =
useSuspenseIndexedDBState<Date>("last-updated", (currentValue) =>
currentValue instanceof Date ? currentValue : new Date()
);
const updateMessage = (newMessage: string) => {
setItem(newMessage);
setLastUpdated(new Date());
};
const clearAll = () => {
deleteItem();
setLastUpdated(new Date());
};
return (
<div>
<h2>Cross-Tab Sync Demo</h2>
<p>Open this page in multiple tabs to see real-time synchronization!</p>
<div>
<strong>Current Message:</strong> {message}
</div>
<div>
<strong>Last Updated:</strong> {lastUpdated.toLocaleTimeString()}
</div>
<input
value={message}
onChange={(e) => updateMessage(e.target.value)}
placeholder="Type a message..."
/>
<button onClick={() => updateMessage("Updated from tab!")}>
Update Message
</button>
<button onClick={clearAll}>Clear Message</button>
<p>
<em>
Try changing the message in one tab and watch it update in others!
</em>
</p>
</div>
);
}Parameters
| Argument | Type | Description | Required |
|---|---|---|---|
| key | string | The key to use within the IndexedDB object store | Yes |
| initializer | (currentValue: unknown) => T | Function that transforms the raw IndexedDB value into typed state | Yes |
| config | { dbName?: string; storeName?: string; version?: number } | Database configuration | No |
Configuration fields
| Property | Type | Default | Description |
|---|---|---|---|
| dbName | string | "rooks-db" | The IndexedDB database name |
| storeName | string | "state" | The object store name within the DB |
| version | number | 1 | The database version |
Return value
Returns a tuple containing:
currentValue: T- The current state value of type Tcontrols: object- An object containing:getItem(): T- Get the current value from statesetItem(value: T): Promise<void>- Set a new value (async)deleteItem(): Promise<void>- Delete the item and reset to initial value (async)
Type Safety
The hook is fully type-safe and supports TypeScript generics:
// The hook infers the type from your initializer function
const [user, controls] = useSuspenseIndexedDBState(
'user',
(value): UserProfile => value || defaultUser
);
// `user` is of type UserProfile
// `controls.setItem` expects UserProfile
// `controls.getItem()` returns UserProfile
// You can also be explicit about the type
const [data, { setItem }] = useSuspenseIndexedDBState<MyDataType>(
'my-data',
(value) => value || getDefaultData()
);Error Handling
The hook handles various error scenarios gracefully:
- IndexedDB not supported: Falls back to in-memory state
- Database opening errors: Throws errors that can be caught by error boundaries
- Transaction errors: Logs errors and maintains existing state
- Initializer errors: Throws errors that can be caught by error boundaries
import React, { Suspense } from "react";
import { useSuspenseIndexedDBState } from "rooks/experimental";
function ErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
</ErrorBoundary>
);
}IndexedDB vs localStorage Comparison
| Feature | useSuspenseIndexedDBState | useSuspenseLocalStorageState |
|---|---|---|
| Data Types | Native objects, binary data | JSON serializable only |
| Storage Limit | ~1GB+ (browser dependent) | ~5-10MB |
| Performance | Better for large data | Better for small data |
| Cross-tab Sync | BroadcastChannel API | Storage events |
| Browser Support | Modern browsers | All browsers |
| Async Operations | Yes (Promises) | No (synchronous) |
| Complex Objects | Native support | JSON serialization required |
Browser Support
- IndexedDB: Supported in all modern browsers (IE 10+)
- BroadcastChannel: Supported in modern browsers (Chrome 54+, Firefox 38+, Safari 15.4+)
- Fallback: When BroadcastChannel is not available, cross-tab sync is disabled but the hook still works
Import
import { useSuspenseIndexedDBState } from "rooks/experimental";Behavior and lifecycle
Initialization is cached by database, store, key, and version and suspends until the database read and initializer finish. Each read/write/delete opens a database connection and closes it when the transaction completes. setItem and deleteItem update local state only after the transaction succeeds, then broadcast to other browsing contexts; the shared BroadcastChannel listener is closed on cleanup.
Compatibility and accessibility
When IndexedDB is missing or the initial read fails, the implementation logs the failure and calls initializer(null) rather than surfacing a storage error. Initializer failures suspend into the nearest error boundary; write and delete failures reject their returned promises. Cross-tab updates require BroadcastChannel support. Treat stored values as untrusted after deployment migrations and do not store secrets merely because the API is origin-scoped.