Experimental Hooks
useSuspenseIndexedDBState
About
⚠️ Experimental Hook: This hook may be removed or significantly changed in any release without notice.
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";
function CustomDBExample() {
const [data, { setItem }] = useSuspenseIndexedDBState(
'my-data',
(currentValue) => currentValue || { 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(
'sync-message',
(currentValue) => currentValue || 'Hello World'
);
const [lastUpdated, { setItem: setLastUpdated }] = useSuspenseIndexedDBState(
'last-updated',
(currentValue) => 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>
);
}Arguments
| 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 | IndexedDBConfig | Optional configuration object | No |
IndexedDBConfig
| 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";