Skip to content

React

The React integration lives at @will-be-done/hyperdb/react. It provides a context provider plus hooks for reactive reads, dispatching actions, and one-off reads. React 18 is a peer dependency.

HyperDB hands your components plain, immutable rows: frozen data, never proxies. There is no observer() wrapper to remember and nothing leaking into your view layer. The selector hooks use HyperDB’s range-tracked reads, so a component re-runs only when a mutation touches a range its selector actually scanned, giving fine-grained updates that compose with React’s rendering model directly.

Wrap your tree in DBProvider and pass a SubscribableDB as its value. Every hook reads the database from this context.

import { DBProvider } from "@will-be-done/hyperdb/react";
import { db } from "./db";
export function App() {
return (
<DBProvider value={db}>
<Tasks projectId="p1" />
</DBProvider>
);
}

useDB() returns the database from context (and throws if there’s no provider). It is useful for passing the DB to non-hook utilities.

Use useAsyncSelector with HybridDB, IndexedDB, and async SQLite. This is the usual hook for local-first browser apps: the last resolved result can stay visible while HybridDB loads missing index ranges from the primary store.

import { useAsyncSelector } from "@will-be-done/hyperdb/react";
import { projectTasks } from "./tasks";
function Tasks({ projectId }: { projectId: string }) {
const {
data: tasks = [],
error,
isFetching,
isLoading,
isError,
refetch,
} = useAsyncSelector({
selector: projectTasks,
args: { projectId },
defaultValue: [],
});
if (isError) return <p>{String(error)}</p>;
const showSpinner = isLoading || isFetching;
return (
<>
{showSpinner ? <span>Loading...</span> : null}
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
<button onClick={() => void refetch()}>Refresh</button>
</>
);
}

The hook accepts the selector and args to run, then returns a query-style result object with data, status, error, fetching flags, and refetch(). Each run starts against the current cache. If a selector needs IndexedDB, async SQLite, or a HybridDB primary-store fallback, the run continues asynchronously. The initial snapshot attempts the selector once: if it finishes synchronously, that value is visible immediately; if it returns a promise, the hook shows defaultValue, initialData, or placeholderData until the same promise resolves. It does not read a cached selector snapshot during render.

useAsyncSelector is built on the framework-agnostic createCachedSelectorStoreAsync from @will-be-done/hyperdb. Use that store directly when you need the same async selector subscription behavior outside React.

Options:

OptionDescription
selectorThe selector to run
argsIts arguments (also the reactive identity)
enabledSet false to skip automatic runs; call refetch() to run manually
defaultValueValue or thunk used as placeholder data before the first resolved run
initialDataInitial successful data for the result
initialDataUpdatedAtTimestamp for initialData
placeholderDataTemporary data while the selector is still pending
subscribedSet false to avoid automatic runs and DB subscriptions for this instance
throwOnErrorThrow render-phase errors to an error boundary when true or a predicate

Returns:

FieldDescription
dataLast successful selector result, placeholder data, initial data, or undefined
status"pending", "success", or "error"
fetchStatus"fetching" or "idle" ("paused" is reserved for query compatibility)
errorLast selector error, or null
dataUpdatedAt / errorUpdatedAtTimestamps for the last success or error
isPending / isSuccess / isErrorStatus booleans
isFetching / isLoading / isRefetchingFetching booleans
isLoadingError / isRefetchErrorDistinguish first-load failures from refresh failures
isPlaceholderData / isStale / isEnabledExtra query-state booleans
failureCount / failureReason / errorUpdateCountFailure counters and reason
promisePromise for the current run’s data
refetch(options)Manually rerun the selector; pass { throwOnError: true } to reject on error

For route loaders or intent prefetches, use preloadSelectorAsync with the same SubscribableDB, selector, and args the component will render. With HybridDB it loads missing primary-store ranges and primes the selector cache that useAsyncSelector can reuse when its async run starts.

import {
preloadSelectorAsync,
type SubscribableDB,
} from "@will-be-done/hyperdb";
import { projectTasks } from "./tasks";
export async function preloadProjectRoute(
db: SubscribableDB,
projectId: string,
) {
await preloadSelectorAsync(db, {
selector: projectTasks,
args: { projectId },
});
}

Use useSyncSelector with synchronous drivers: in-memory or sync SQLite. It is the simplest path when your app can load its whole working state into memory, so there are no promise or cache-miss tradeoffs.

import { useSyncSelector } from "@will-be-done/hyperdb/react";
import { projectTasks } from "./tasks";
function Tasks({ projectId }: { projectId: string }) {
const tasks = useSyncSelector({
selector: projectTasks,
args: { projectId },
defaultValue: [],
});
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}

Options:

OptionDescription
selectorThe selector to run
argsIts arguments (also the cache key)
defaultValueValue returned before the first result / when disabled
enabledSet false to skip running; returns defaultValue

Return a function that dispatches an action against the context database.

import { useAsyncDispatch } from "@will-be-done/hyperdb/react";
import { createTask } from "./tasks";
function AddButton({ projectId }: { projectId: string }) {
const dispatch = useAsyncDispatch();
return (
<button
onClick={() =>
void dispatch(
createTask({ id: crypto.randomUUID(), projectId, title: "New" }),
)
}
>
Add
</button>
);
}

Use useAsyncDispatch with HybridDB, IndexedDB, and async SQLite. Use useSyncDispatch only with synchronous drivers. With HybridDB, writes update the in-memory cache first so subscribed UI can respond immediately, then flush to the primary store in order.

useSelectSync and useSelectAsync return a function for imperative, non-reactive reads, for example reading inside an event handler. They don’t subscribe.

import { useSelectAsync } from "@will-be-done/hyperdb/react";
const select = useSelectAsync();
const handleClick = async () => {
const tasks = await select({
selector: projectTasks,
args: { projectId: "p1" },
});
};

Use useSelectSync for synchronous drivers.

HookReturnsUse with
useDB()the SubscribableDBaccessing the DB directly
useAsyncSelector(opts)query-style result objectHybridDB, IndexedDB, async SQLite
useSyncSelector(opts)the selector resultin-memory and sync SQLite
useAsyncDispatch()(action) => Promise<TReturn>HybridDB, IndexedDB, async SQLite
useSyncDispatch()(action) => TReturnin-memory and sync SQLite
useSelectAsync()({ selector, args }) => Promise<TReturn>one-off read, async runtimes
useSelectSync()({ selector, args }) => TReturnone-off read, sync runtimes