Skip to content

@syncraft-labs/react

@syncraft-labs/react provides the useSync and useSyncSuspense hooks — giving your React components instant writes, IndexedDB persistence, background sync, and offline support.

Built on useSyncExternalStore for tear-free concurrent rendering.

Install

Terminal window
npm install @syncraft-labs/core @syncraft-labs/react

Peer dependencies: React ≥ 18.0.0

Quick Start

Wrap your app with <SyncraftProvider> (required for store registry isolation across requests in SSR):

import { SyncraftProvider, useSync } from "@syncraft-labs/react";
interface TodoState {
todos: Array<{ id: string; text: string; done: boolean }>;
}
function TodoApp() {
const { data, update, isHydrating, isOffline, error } = useSync<TodoState>(
"todos",
{
initialState: { todos: [] },
},
);
if (isHydrating) return <p>Loading from cache…</p>;
return (
<div>
{isOffline && <p>You're offline — changes saved locally</p>}
{error && <p>Error: {error.message}</p>}
<button
onClick={() =>
update((draft) => {
draft.todos.push({
id: crypto.randomUUID(),
text: "New todo",
done: false,
});
})
}
>
Add Todo
</button>
<ul>
{data?.todos.map((t) => (
<li key={t.id}>
<label>
<input
type="checkbox"
checked={t.done}
onChange={() =>
update((draft) => {
const todo = draft.todos.find((x) => x.id === t.id);
if (todo) todo.done = !todo.done;
})
}
/>
{t.text}
</label>
</li>
))}
</ul>
</div>
);
}
export default function App() {
return (
<SyncraftProvider>
<TodoApp />
</SyncraftProvider>
);
}

API Reference

SyncraftProvider

Context provider that initializes and scopes the Store Registry for your React component tree. Required to prevent cross-request state leakage during SSR.

<SyncraftProvider>
<App />
</SyncraftProvider>

useSync<T>(key, options): UseSyncReturn<T>

Standard React hook for subscribing to a SyncStore.

ParameterTypeDescription
keystringUnique IndexedDB storage key
optionsUseSyncOptions<T>Configuration object

UseSyncOptions<T>

OptionTypeDefaultDescription
initialStateTundefinedDefault state when IndexedDB is empty
fetcher() => Promise<T>undefinedFetch initial data from remote source
pusher(entries: OutboxEntry<T>[]) => Promise<void>undefinedPush pending mutations to server
syncIntervalnumber5000Background sync interval (ms)
maxOutboxSizenumber1000Maximum outbox entries before overflow strategy triggers
overflowStrategy"reject" | "dropOldest" | "forceFlush""reject"Behavior when maxOutboxSize is reached
onOverflow(info: OutboxOverflowInfo) => void | Promise<void>undefinedCallback invoked on outbox overflow events
storageMode"document" | "collection""document"Storage strategy ("document" or "collection")
idFieldstringundefinedProperty name of entity ID (required for collection mode)

UseSyncReturn<T>

PropertyTypeDescription
dataT | undefinedCurrent state (undefined during hydration)
update(updater: DraftUpdater<T>) => voidMutate state with draft (fire-and-forget)
refetch() => Promise<void>Pull fresh data via fetcher
isHydratingbooleantrue while loading from IndexedDB
isSyncingbooleantrue while pusher/refetch is running
isOfflinebooleantrue when navigator.onLine is false
errorError | nullLast error from set/pusher/refetch
destroyStore() => voidDestroy the store for this key

useSyncSuspense<T>(key, options): UseSyncReturn<T>

React Suspense-compatible version of useSync. Suspends component rendering while the store hydrates from IndexedDB or initial fetcher is executing.

<React.Suspense fallback={<Skeleton />}>
<TodoAppWithSuspense />
</React.Suspense>

destroyStore(registry, key): void

Destroy a store and remove it from the specified registry. Closes IndexedDB connection and clears listeners.