Reactive · Local-first · Typed

IndexedDB,
finally as React hooks.

Persistent, reactive browser storage with built-in cross-tab sync — and zero runtime dependencies.

2.4 KB gzipped 0 dependencies React 16.8 – 19
install
npm install react-idb-hooks
// read — re-renders on every change, even from other tabs
const { data } = useIDBQuery(db, (d) => d.getAll("todos"), ["todos"]);

// write — typed, awaitable, commit-safe
const { mutate } = useIDBMutation(db, "todos");
The Tradeoff Map

Reactive & cross-tab, without the weight.

Dexie React Hooks is the only other library with live queries today — and it ships ~65 KB. Here is how the options actually stack up.

Feature comparison of react-idb-hooks against dexie-react-hooks, use-indexeddb, and raw idb
Capability react-idb-hooks dexie-react-hooks use-indexeddb idb (raw)
Bundle (gzip) 2.4 KB ~65 KB ~3 KB ~3 KB
Runtime dependencies 0 Dexie 0 0
Reactive React hooks Yes Yes Partial No
Cross-tab sync built in Yes Manual No No
Schema-typed reads & writes Yes Yes Partial Minimal
Migrations API Yes Yes Partial Manual
SSR-safe (Next, Remix) Yes With care With care With care

Need a full ORM with joins and a query language? Reach for Dexie. Need a single key/value blob? Use localStorage. This library is the sweet spot for typed, reactive, multi-tab state.

Copy. Paste. Persist.

From zero to reactive storage in four steps.

No provider to mount, no atom registry, no class to subclass. Define a schema once, then read and write with hooks.

Define your database

Call defineIDB once at module scope. The schema interface is your single source of truth — store names, keys, value shapes, and index names are all inferred from it.

src/db.ts
import { defineIDB } from "react-idb-hooks";

interface AppSchema {
  todos: {
    value: { id: string; title: string; done: boolean };
    key: string;
    indexes: { byDone: 0 | 1 };
  };
}

export const db = defineIDB<AppSchema>({
  name: "my-app",
  version: 1,
  upgrade({ db, oldVersion }) {
    // runs once per version bump
    if (oldVersion < 1) {
      const todos = db.createObjectStore("todos", { keyPath: "id" });
      todos.createIndex("byDone", "done");
    }
  },
});

Read with useIDBQuery

Pass any async function over the typed database and the stores it depends on. When anything mutates those stores — in this component, another component, or another tab — the query re-runs.

TodoList.tsx
import { useIDBQuery } from "react-idb-hooks";
import { db } from "./db";

function TodoList() {
  const { data, status, error } = useIDBQuery(
    db,
    (d) => d.getAll("todos"),  // any async read
    ["todos"],                 // re-run on these stores
  );

  if (status === "loading") return <p>Loading…</p>;
  if (status === "error") return <p>{error.message}</p>;
  return <ul>{data.map((t) => <li key={t.id}>{t.title}</li>)}</ul>;
}

Why an explicit dep list? ["todos"] is visible in code review and impossible to misuse — no Proxy magic, no surprise re-renders.

Write with useIDBMutation

mutate resolves after the IndexedDB transaction commits and the local & cross-tab invalidations fire. Status flows idle → pending → success | error.

AddTodo.tsx
import { useIDBMutation } from "react-idb-hooks";
import { db } from "./db";

function AddTodo() {
  const { mutate, status } = useIDBMutation(db, "todos");

  return (
    <button
      disabled={status === "pending"}
      onClick={() =>
        mutate({
          type: "put",
          value: { id: crypto.randomUUID(), title: "New", done: false },
        })
      }
    >
      Add Todo
    </button>
  );
}

Watch the connection with useIDB

Render skeletons, offline banners, or error states straight from the connection status — including "unsupported" for SSR, React Native, and private-mode browsers.

StatusDot.tsx
import { useIDB } from "react-idb-hooks";
import { db } from "./db";

function StatusDot() {
  const { status } = useIDB(db);
  // "loading" | "ready" | "error" | "unsupported"
  return <span data-state={status} />;
}
Small Surface, Big Guarantees

Seven files you can read in fifteen minutes.

Zero Runtime Dependencies

Nothing transitive to audit. The useSyncExternalStore shim is vendored, so React is the only peer.

Cross-Tab Sync, Built In

Mutate in one tab; every other tab re-renders. Powered by BroadcastChannel — no storage-event hacks.

Inferred, Not Generated

One interface types every read and write. No codegen, no runtime declarations, no build step.

2.4 KB Gzipped

Live queries without the 65 KB ORM. 6.1 KB min · 2.4 KB gzip

React 16.8 – 19

Native useSyncExternalStore on 18 / 19, vendored shim on 16 / 17. Strict-Mode tested.

SSR-Safe & Typed Errors

Lazy connection, no import-time open. Every recoverable failure maps to an instanceof-able class.

Open Source · MIT

If it saves you an afternoon,
leave a star.

Stars help other developers find a small, auditable alternative to heavyweight storage libraries. It takes one click.

github.com/rully-saputra15/react-idb-hooks