Fiber UI LogoFiberUI

useIndexedDB

A hook for storing large amounts of structured data in IndexedDB

A React hook that provides CRUD operations for IndexedDB, the browser's built-in database for storing large amounts of structured data. Unlike localStorage which is limited to ~5MB of string data, IndexedDB can store gigabytes of any data type including objects, arrays, files, and blobs.

Source Code

View the full hook implementation in the Hook Source Code section below.

What is IndexedDB?

IndexedDB is a low-level API for client-side storage of structured data. It's perfect for:

  • Offline-first applications - Cache API responses for offline access
  • Large datasets - Store thousands of records without hitting size limits
  • Complex data - Store objects with nested structures, arrays, and relationships
  • Binary data - Store files, images, and blobs directly

Verify in DevTools!

Open Chrome DevTools → Application tab → IndexedDB (under Storage) to see your data. You can view, edit, and delete records directly!

Features

  • Full CRUD - add, update, remove, getByKey, clear operations
  • SSR Safe - Checks for IndexedDB support before initialization
  • Type Safe - Full TypeScript support with generics
  • Auto Refresh - Automatically refreshes data after mutations
  • Error Handling - Comprehensive error state management
  • Large Data - Handle megabytes of data (not limited like localStorage)

Learn More


Basic Usage - Notes App

A simple notes application that persists notes to IndexedDB:

Try it out!

Add some notes, then close the browser completely and reopen - your notes persist! Check DevTools → Application → IndexedDB → notesApp to see the stored data.

Loading...
"use client";

import { useState } from "react";
import { useIndexedDB } from "@repo/hooks/storage/use-indexed-db";

interface Note {
    id: string;
    title: string;
    content: string;
    createdAt: number;
}

/* BASIC USAGE - Notes App */
export const Example1 = () => {
    const {
        data: notes,
        add,
        remove,
        isLoading,
        isSupported,
        error,
    } = useIndexedDB<Note>({
        dbName: "notesApp",
        storeName: "notes",
        keyPath: "id",
    });

    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");

    if (!isSupported) {
        return (
            <div className="rounded-md border border-yellow-500/50 bg-yellow-500/10 p-4">
                <p className="text-sm text-yellow-600 dark:text-yellow-400">
                    IndexedDB is not supported in this browser
                </p>
            </div>
        );
    }

    if (isLoading) {
        return (
            <span className="text-muted-foreground text-sm">Loading...</span>
        );
    }

    const handleAdd = async () => {
        if (!title.trim()) return;
        await add({
            id: crypto.randomUUID(),
            title,
            content,
            createdAt: Date.now(),
        });
        setTitle("");
        setContent("");
    };

    return (
        <div className="flex w-full max-w-md flex-col gap-4">
            {error && <p className="text-destructive text-sm">{error}</p>}
            <div className="flex flex-col gap-2">
                <input
                    className="bg-background rounded-md border px-3 py-2 text-sm"
                    value={title}
                    onChange={(e) => setTitle(e.target.value)}
                    placeholder="Note title..."
                />
                <textarea
                    className="bg-background rounded-md border px-3 py-2 text-sm"
                    value={content}
                    onChange={(e) => setContent(e.target.value)}
                    placeholder="Note content..."
                    rows={2}
                />
                <button
                    className="bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm"
                    onClick={handleAdd}
                >
                    Add Note
                </button>
            </div>
            <div className="flex flex-col gap-2">
                {notes.length === 0 ? (
                    <p className="text-muted-foreground text-sm">
                        No notes yet
                    </p>
                ) : (
                    notes.map((note) => (
                        <div
                            key={note.id}
                            className="flex items-start justify-between rounded-md border p-3"
                        >
                            <div>
                                <h4 className="text-sm font-medium">
                                    {note.title}
                                </h4>
                                <p className="text-muted-foreground text-xs">
                                    {note.content}
                                </p>
                            </div>
                            <button
                                className="text-destructive hover:text-destructive/80"
                                onClick={() => remove(note.id)}
                            >
                                ×
                            </button>
                        </div>
                    ))
                )}
            </div>
        </div>
    );
};

Shopping Cart

A persistent shopping cart that survives browser restarts. Demonstrates add, update, remove, and clear operations:

Try it out!

Add products to cart, adjust quantities, then reload the page - your cart is preserved! View the data in DevTools → Application → IndexedDB → shopApp → cart.

Loading...
"use client";

import { useIndexedDB } from "@repo/hooks/storage/use-indexed-db";

interface Product {
    id: string;
    name: string;
    price: number;
    quantity: number;
}

/* SHOPPING CART - Persistent Cart */
export const Example2 = () => {
    const {
        data: cart,
        add,
        update,
        remove,
        clear,
        isLoading,
        isSupported,
    } = useIndexedDB<Product>({
        dbName: "shopApp",
        storeName: "cart",
        keyPath: "id",
    });

    if (!isSupported) {
        return (
            <div className="rounded-md border border-yellow-500/50 bg-yellow-500/10 p-4">
                <p className="text-sm text-yellow-600 dark:text-yellow-400">
                    IndexedDB is not supported
                </p>
            </div>
        );
    }

    if (isLoading) {
        return (
            <span className="text-muted-foreground text-sm">Loading...</span>
        );
    }

    const sampleProducts = [
        { id: "p1", name: "Laptop", price: 999, quantity: 1 },
        { id: "p2", name: "Mouse", price: 29, quantity: 1 },
        { id: "p3", name: "Keyboard", price: 79, quantity: 1 },
    ];

    const addToCart = async (product: Product) => {
        const existing = cart.find((p) => p.id === product.id);
        if (existing) {
            await update({ ...existing, quantity: existing.quantity + 1 });
        } else {
            await add(product);
        }
    };

    const updateQuantity = async (id: string, delta: number) => {
        const item = cart.find((p) => p.id === id);
        if (!item) return;
        if (item.quantity + delta <= 0) {
            await remove(id);
        } else {
            await update({ ...item, quantity: item.quantity + delta });
        }
    };

    const total = cart.reduce(
        (sum, item) => sum + item.price * item.quantity,
        0,
    );

    return (
        <div className="flex w-full max-w-md flex-col gap-4">
            {/* Products */}
            <div className="flex flex-wrap gap-2">
                {sampleProducts.map((product) => (
                    <button
                        key={product.id}
                        className="hover:bg-muted rounded-md border px-3 py-1.5 text-sm"
                        onClick={() => addToCart(product)}
                    >
                        {product.name} (${product.price})
                    </button>
                ))}
            </div>

            {/* Cart */}
            <div className="rounded-md border">
                <div className="bg-muted/50 border-b px-4 py-2">
                    <span className="text-sm font-medium">
                        Cart ({cart.length} items)
                    </span>
                </div>
                {cart.length === 0 ? (
                    <p className="text-muted-foreground p-4 text-sm">
                        Cart is empty
                    </p>
                ) : (
                    <div className="divide-y">
                        {cart.map((item) => (
                            <div
                                key={item.id}
                                className="flex items-center justify-between px-4 py-2"
                            >
                                <span className="text-sm">{item.name}</span>
                                <div className="flex items-center gap-2">
                                    <button
                                        className="bg-muted rounded px-2 py-0.5 text-xs"
                                        onClick={() =>
                                            updateQuantity(item.id, -1)
                                        }
                                    >
                                        -
                                    </button>
                                    <span className="text-sm">
                                        {item.quantity}
                                    </span>
                                    <button
                                        className="bg-muted rounded px-2 py-0.5 text-xs"
                                        onClick={() =>
                                            updateQuantity(item.id, 1)
                                        }
                                    >
                                        +
                                    </button>
                                    <span className="ml-2 text-sm font-medium">
                                        ${item.price * item.quantity}
                                    </span>
                                </div>
                            </div>
                        ))}
                        <div className="bg-muted/50 flex items-center justify-between px-4 py-2">
                            <span className="text-sm font-medium">
                                Total: ${total}
                            </span>
                            <button
                                className="text-destructive text-xs hover:underline"
                                onClick={clear}
                            >
                                Clear Cart
                            </button>
                        </div>
                    </div>
                )}
            </div>
        </div>
    );
};

Bookmarks with Tags

A bookmarks manager demonstrating complex object updates with arrays:

Try it out!

Add bookmarks and toggle tags. The update function replaces the entire object, allowing you to modify nested data like tag arrays.

Loading...
"use client";

import { useIndexedDB } from "@repo/hooks/storage/use-indexed-db";

interface Bookmark {
    id: string;
    url: string;
    title: string;
    tags: string[];
}

/* BOOKMARKS - With Tags */
export const Example3 = () => {
    const {
        data: bookmarks,
        add,
        remove,
        update,
        isLoading,
        isSupported,
    } = useIndexedDB<Bookmark>({
        dbName: "bookmarksApp",
        storeName: "bookmarks",
        keyPath: "id",
    });

    if (!isSupported) {
        return (
            <div className="rounded-md border border-yellow-500/50 bg-yellow-500/10 p-4">
                <p className="text-sm text-yellow-600 dark:text-yellow-400">
                    IndexedDB is not supported
                </p>
            </div>
        );
    }

    if (isLoading) {
        return (
            <span className="text-muted-foreground text-sm">Loading...</span>
        );
    }

    const sampleBookmarks: Bookmark[] = [
        {
            id: "1",
            url: "https://react.dev",
            title: "React Docs",
            tags: ["react", "docs"],
        },
        {
            id: "2",
            url: "https://nextjs.org",
            title: "Next.js",
            tags: ["nextjs", "framework"],
        },
        {
            id: "3",
            url: "https://github.com",
            title: "GitHub",
            tags: ["git", "code"],
        },
    ];

    const addBookmark = (bookmark: Bookmark) => {
        if (bookmarks.some((b) => b.id === bookmark.id)) return;
        add(bookmark);
    };

    const toggleTag = async (bookmark: Bookmark, tag: string) => {
        const newTags = bookmark.tags.includes(tag)
            ? bookmark.tags.filter((t) => t !== tag)
            : [...bookmark.tags, tag];
        await update({ ...bookmark, tags: newTags });
    };

    return (
        <div className="flex w-full max-w-md flex-col gap-4">
            {/* Add Buttons */}
            <div className="flex flex-wrap gap-2">
                {sampleBookmarks.map((b) => (
                    <button
                        key={b.id}
                        className="hover:bg-muted rounded-md border px-3 py-1.5 text-xs"
                        onClick={() => addBookmark(b)}
                    >
                        + {b.title}
                    </button>
                ))}
            </div>

            {/* Bookmarks List */}
            <div className="flex flex-col gap-2">
                {bookmarks.length === 0 ? (
                    <p className="text-muted-foreground text-sm">
                        No bookmarks saved
                    </p>
                ) : (
                    bookmarks.map((bookmark) => (
                        <div
                            key={bookmark.id}
                            className="flex flex-col gap-2 rounded-md border p-3"
                        >
                            <div className="flex items-center justify-between">
                                <a
                                    href={bookmark.url}
                                    className="text-primary text-sm font-medium hover:underline"
                                    target="_blank"
                                    rel="noopener noreferrer"
                                >
                                    {bookmark.title}
                                </a>
                                <button
                                    className="text-destructive hover:text-destructive/80"
                                    onClick={() => remove(bookmark.id)}
                                >
                                    ×
                                </button>
                            </div>
                            <div className="flex flex-wrap gap-1">
                                {["react", "nextjs", "docs", "code"].map(
                                    (tag) => (
                                        <button
                                            key={tag}
                                            className={`rounded-full px-2 py-0.5 text-xs ${
                                                bookmark.tags.includes(tag)
                                                    ? "bg-primary text-primary-foreground"
                                                    : "bg-muted text-muted-foreground"
                                            }`}
                                            onClick={() =>
                                                toggleTag(bookmark, tag)
                                            }
                                        >
                                            {tag}
                                        </button>
                                    ),
                                )}
                            </div>
                        </div>
                    ))
                )}
            </div>
        </div>
    );
};

How It Works

Database Structure

IndexedDB organizes data in a hierarchy:

Database (dbName: "myApp")
  └── Object Store (storeName: "users")
        ├── Record { id: "1", name: "Alice", ... }
        ├── Record { id: "2", name: "Bob", ... }
        └── Record { id: "3", name: "Charlie", ... }

Key Path

The keyPath option defines which property is used as the primary key:

// Using "id" as the key (default)
useIndexedDB<User>({
    dbName: "myApp",
    storeName: "users",
    keyPath: "id", // Each record must have an "id" property
});

Version Management

Increment version when you need to modify the store structure:

useIndexedDB<User>({
    dbName: "myApp",
    storeName: "users",
    version: 2, // Triggers onupgradeneeded event
});

API Reference

function useIndexedDB<T>(options: UseIndexedDBOptions): UseIndexedDBReturn<T>;

Options

PropertyTypeDefaultDescription
dbNamestring-Name of the IndexedDB database
storeNamestring-Name of the object store within the database
versionnumber1Database version (increment to trigger upgrades)
keyPathstring"id"Property used as the primary key for records

Return Value

PropertyTypeDescription
dataT[]All items currently in the store
isLoadingbooleantrue while database is initializing
errorstring | nullError message if an operation failed
isSupportedbooleanfalse if IndexedDB is unavailable
add(item: T) => Promise<IDBValidKey|null>Insert a new item into the store
update(item: T) => Promise<IDBValidKey|null>Replace an existing item (by key)
remove(key: IDBValidKey) => Promise<boolean>Delete an item by its key
getByKey(key: IDBValidKey) => Promise<T|null>Fetch a single item by key
clear() => Promise<boolean>Remove all items from the store
refresh() => Promise<void>Manually reload data from the store

When to Use Each Storage Hook

ScenarioRecommended HookReason
User preferences (theme)useLocalStorageStateSimple, synchronous, small data
Form data during sessionuseSessionStorageStateClears when tab closes
Shopping cartuseIndexedDBComplex objects, survives restarts
Offline data cacheuseIndexedDBLarge datasets, fast queries
User documents/notesuseIndexedDBPersistent, structured data
Temporary filtersuseSessionStorageStateSession-scoped, resets on new visit

Comparison with Other Storage

FeaturelocalStoragesessionStorageIndexedDB
Storage Limit~5MB~5MBGBs (quota-based)
Data TypesStrings onlyStrings onlyAny (objects, blobs)
API StyleSynchronousSynchronousAsynchronous
PersistenceForeverUntil tab closesForever
Cross-tabSharedIsolatedShared
PerformanceFast for small dataFast for small dataFast for any size

Hook Source Code

import { useState, useEffect, useCallback } from "react";

/**
 * IndexedDB hook state
 */
export interface UseIndexedDBState<T> {
    /** Data retrieved from the store */
    data: T[];
    /** Whether the database is loading */
    isLoading: boolean;
    /** Error message if any */
    error: string | null;
    /** Whether IndexedDB is supported */
    isSupported: boolean;
}

/**
 * IndexedDB hook return type
 */
export interface UseIndexedDBReturn<T> extends UseIndexedDBState<T> {
    /** Add an item to the store */
    add: (item: T) => Promise<IDBValidKey | null>;
    /** Update an item in the store */
    update: (item: T) => Promise<IDBValidKey | null>;
    /** Delete an item by key */
    remove: (key: IDBValidKey) => Promise<boolean>;
    /** Get a single item by key */
    getByKey: (key: IDBValidKey) => Promise<T | null>;
    /** Clear all items from the store */
    clear: () => Promise<boolean>;
    /** Refresh data from the store */
    refresh: () => Promise<void>;
}

/**
 * IndexedDB configuration options
 */
export interface UseIndexedDBOptions {
    /** Name of the database */
    dbName: string;
    /** Name of the object store */
    storeName: string;
    /** Database version (increment to trigger upgrade) */
    version?: number;
    /** Key path for the object store */
    keyPath?: string;
}

/**
 * Opens an IndexedDB database
 */
function openDatabase(
    dbName: string,
    storeName: string,
    version: number,
    keyPath: string,
): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(dbName, version);

        request.onerror = () => {
            reject(
                new Error(`Failed to open database: ${request.error?.message}`),
            );
        };

        request.onsuccess = () => {
            resolve(request.result);
        };

        request.onupgradeneeded = (event) => {
            const db = (event.target as IDBOpenDBRequest).result;
            if (!db.objectStoreNames.contains(storeName)) {
                db.createObjectStore(storeName, {
                    keyPath,
                    autoIncrement: !keyPath,
                });
            }
        };
    });
}

/**
 * A React hook for interacting with IndexedDB.
 * Provides CRUD operations for storing large amounts of structured data.
 *
 * @param options - Configuration options for the database
 * @returns State and methods for interacting with IndexedDB
 *
 * @example
 * ```tsx
 * interface Todo {
 *     id: string;
 *     text: string;
 *     completed: boolean;
 * }
 *
 * const { data, add, remove, isLoading } = useIndexedDB<Todo>({
 *     dbName: "myApp",
 *     storeName: "todos",
 *     keyPath: "id",
 * });
 * ```
 */
export function useIndexedDB<T>(
    options: UseIndexedDBOptions,
): UseIndexedDBReturn<T> {
    const { dbName, storeName, version = 1, keyPath = "id" } = options;

    const [state, setState] = useState<UseIndexedDBState<T>>({
        data: [],
        isLoading: true,
        error: null,
        isSupported: true,
    });

    const [db, setDb] = useState<IDBDatabase | null>(null);

    // Initialize database
    useEffect(() => {
        if (typeof window === "undefined" || !("indexedDB" in window)) {
            setState((s) => ({
                ...s,
                isSupported: false,
                isLoading: false,
                error: "IndexedDB is not supported",
            }));
            return;
        }

        let mounted = true;

        (async () => {
            try {
                const database = await openDatabase(
                    dbName,
                    storeName,
                    version,
                    keyPath,
                );
                if (mounted) {
                    setDb(database);
                }
            } catch (error) {
                if (mounted) {
                    setState((s) => ({
                        ...s,
                        isLoading: false,
                        error:
                            error instanceof Error
                                ? error.message
                                : "Failed to open database",
                    }));
                }
            }
        })();

        return () => {
            mounted = false;
        };
    }, [dbName, storeName, version, keyPath]);

    // Fetch all data when database is ready
    const refresh = useCallback(async () => {
        if (!db) return;

        setState((s) => ({ ...s, isLoading: true }));

        try {
            const transaction = db.transaction(storeName, "readonly");
            const store = transaction.objectStore(storeName);
            const request = store.getAll();

            request.onsuccess = () => {
                setState((s) => ({
                    ...s,
                    data: request.result,
                    isLoading: false,
                    error: null,
                }));
            };

            request.onerror = () => {
                setState((s) => ({
                    ...s,
                    isLoading: false,
                    error: request.error?.message ?? "Failed to fetch data",
                }));
            };
        } catch (error) {
            setState((s) => ({
                ...s,
                isLoading: false,
                error:
                    error instanceof Error
                        ? error.message
                        : "Failed to fetch data",
            }));
        }
    }, [db, storeName]);

    // Load initial data when db is ready
    useEffect(() => {
        if (db) {
            refresh();
        }
    }, [db, refresh]);

    // Add item
    const add = useCallback(
        async (item: T): Promise<IDBValidKey | null> => {
            if (!db) return null;

            return new Promise((resolve) => {
                try {
                    const transaction = db.transaction(storeName, "readwrite");
                    const store = transaction.objectStore(storeName);
                    const request = store.add(item);

                    request.onsuccess = () => {
                        refresh();
                        resolve(request.result);
                    };

                    request.onerror = () => {
                        setState((s) => ({
                            ...s,
                            error:
                                request.error?.message ?? "Failed to add item",
                        }));
                        resolve(null);
                    };
                } catch (error) {
                    setState((s) => ({
                        ...s,
                        error:
                            error instanceof Error
                                ? error.message
                                : "Failed to add item",
                    }));
                    resolve(null);
                }
            });
        },
        [db, storeName, refresh],
    );

    // Update item
    const update = useCallback(
        async (item: T): Promise<IDBValidKey | null> => {
            if (!db) return null;

            return new Promise((resolve) => {
                try {
                    const transaction = db.transaction(storeName, "readwrite");
                    const store = transaction.objectStore(storeName);
                    const request = store.put(item);

                    request.onsuccess = () => {
                        refresh();
                        resolve(request.result);
                    };

                    request.onerror = () => {
                        setState((s) => ({
                            ...s,
                            error:
                                request.error?.message ??
                                "Failed to update item",
                        }));
                        resolve(null);
                    };
                } catch (error) {
                    setState((s) => ({
                        ...s,
                        error:
                            error instanceof Error
                                ? error.message
                                : "Failed to update item",
                    }));
                    resolve(null);
                }
            });
        },
        [db, storeName, refresh],
    );

    // Remove item
    const remove = useCallback(
        async (key: IDBValidKey): Promise<boolean> => {
            if (!db) return false;

            return new Promise((resolve) => {
                try {
                    const transaction = db.transaction(storeName, "readwrite");
                    const store = transaction.objectStore(storeName);
                    const request = store.delete(key);

                    request.onsuccess = () => {
                        refresh();
                        resolve(true);
                    };

                    request.onerror = () => {
                        setState((s) => ({
                            ...s,
                            error:
                                request.error?.message ??
                                "Failed to delete item",
                        }));
                        resolve(false);
                    };
                } catch (error) {
                    setState((s) => ({
                        ...s,
                        error:
                            error instanceof Error
                                ? error.message
                                : "Failed to delete item",
                    }));
                    resolve(false);
                }
            });
        },
        [db, storeName, refresh],
    );

    // Get by key
    const getByKey = useCallback(
        async (key: IDBValidKey): Promise<T | null> => {
            if (!db) return null;

            return new Promise((resolve) => {
                try {
                    const transaction = db.transaction(storeName, "readonly");
                    const store = transaction.objectStore(storeName);
                    const request = store.get(key);

                    request.onsuccess = () => {
                        resolve(request.result ?? null);
                    };

                    request.onerror = () => {
                        setState((s) => ({
                            ...s,
                            error:
                                request.error?.message ?? "Failed to get item",
                        }));
                        resolve(null);
                    };
                } catch (error) {
                    setState((s) => ({
                        ...s,
                        error:
                            error instanceof Error
                                ? error.message
                                : "Failed to get item",
                    }));
                    resolve(null);
                }
            });
        },
        [db, storeName],
    );

    // Clear all
    const clear = useCallback(async (): Promise<boolean> => {
        if (!db) return false;

        return new Promise((resolve) => {
            try {
                const transaction = db.transaction(storeName, "readwrite");
                const store = transaction.objectStore(storeName);
                const request = store.clear();

                request.onsuccess = () => {
                    refresh();
                    resolve(true);
                };

                request.onerror = () => {
                    setState((s) => ({
                        ...s,
                        error:
                            request.error?.message ?? "Failed to clear store",
                    }));
                    resolve(false);
                };
            } catch (error) {
                setState((s) => ({
                    ...s,
                    error:
                        error instanceof Error
                            ? error.message
                            : "Failed to clear store",
                }));
                resolve(false);
            }
        });
    }, [db, storeName, refresh]);

    return {
        ...state,
        add,
        update,
        remove,
        getByKey,
        clear,
        refresh,
    };
}

export default useIndexedDB;