Rooks
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

ArgumentTypeDescriptionRequired
keystringThe key to use within the IndexedDB object storeYes
initializer(currentValue: unknown) => TFunction that transforms the raw IndexedDB value into typed stateYes
configIndexedDBConfigOptional configuration objectNo

IndexedDBConfig

PropertyTypeDefaultDescription
dbNamestring"rooks-db"The IndexedDB database name
storeNamestring"state"The object store name within the DB
versionnumber1The database version

Return Value

Returns a tuple containing:

  1. currentValue: T - The current state value of type T
  2. controls: object - An object containing:
    • getItem(): T - Get the current value from state
    • setItem(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

FeatureuseSuspenseIndexedDBStateuseSuspenseLocalStorageState
Data TypesNative objects, binary dataJSON serializable only
Storage Limit~1GB+ (browser dependent)~5-10MB
PerformanceBetter for large dataBetter for small data
Cross-tab SyncBroadcastChannel APIStorage events
Browser SupportModern browsersAll browsers
Async OperationsYes (Promises)No (synchronous)
Complex ObjectsNative supportJSON 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";