React
API reference for @lakesync/react — hooks and provider for building reactive local-first React apps.
The @lakesync/react package provides React bindings for LakeSync. A thin reactive layer over the client SDK that re-runs queries automatically when data changes — locally or from remote sync.
Installation
npm install @lakesync/react @lakesync/client @lakesync/core reactOr via the unified package:
npm install lakesync reactimport {
LakeSyncProvider,
useQuery,
useMutation,
useSyncStatus,
useAction,
useActionDiscovery,
} from "lakesync/react";LakeSyncProvider
Wraps your app with LakeSync context. Takes an already-constructed SyncCoordinator — you control transport, config, and lifecycle.
import { LakeSyncProvider } from "@lakesync/react";
function App() {
return (
<LakeSyncProvider coordinator={coordinator}>
<TodoList />
</LakeSyncProvider>
);
}Props
interface LakeSyncProviderProps {
/** An already-constructed SyncCoordinator instance. */
coordinator: SyncCoordinator;
children: React.ReactNode;
}Internally subscribes to coordinator.on("onChange") and maintains a dataVersion counter that increments on every remote delta application. This drives reactivity for all useQuery hooks in the tree.
useLakeSync
Access raw SDK instances from context.
const { coordinator, tracker, dataVersion, invalidate } = useLakeSync();Return value
interface LakeSyncContextValue {
coordinator: SyncCoordinator;
tracker: SyncTracker;
/** Monotonically increasing counter — bumped on every data change. */
dataVersion: number;
/** Increment dataVersion to trigger query re-runs. */
invalidate: () => void;
}Throws if called outside a <LakeSyncProvider>.
useQuery
Reactive SQL query. Re-runs automatically when data changes — remote sync, local mutations, or parameter changes.
const { data, error, isLoading, refetch } = useQuery<Todo>(
"SELECT * FROM todos WHERE done = ?",
[0]
);Parameters
| Parameter | Type | Description |
|---|---|---|
sql | string | SQL query string |
params | unknown[] | Optional bind parameters |
Return value
interface UseQueryResult<T> {
data: T[];
error: DbError | null;
isLoading: boolean;
refetch: () => void;
}data— Query results. Empty array until first load completes.error— Database error from the last query, ornull.isLoading—trueon first render until the query resolves.refetch— Manually trigger a re-run.
Reactivity
The query re-runs when any of these change:
sqlorparamsargumentsdataVersion(incremented by remote sync viaonChange)dataVersion(incremented byuseMutationon successful writes)- Manual
refetch()call
useMutation
Wraps SyncTracker mutations with automatic query invalidation. After each successful mutation, all active useQuery hooks re-run.
const { insert, update, remove } = useMutation();
await insert("todos", crypto.randomUUID(), { text: "Buy milk", done: 0 });
await update("todos", "row-1", { done: 1 });
await remove("todos", "row-1");Return value
interface UseMutationResult {
insert: (
table: string,
rowId: string,
data: Record<string, unknown>,
) => Promise<Result<void, LakeSyncError>>;
update: (
table: string,
rowId: string,
data: Record<string, unknown>,
) => Promise<Result<void, LakeSyncError>>;
remove: (
table: string,
rowId: string,
) => Promise<Result<void, LakeSyncError>>;
}- Named
remove(notdelete) to avoid JS reserved word issues. - Each returns
Result<void, LakeSyncError>— check.okbefore proceeding. - On success,
dataVersionincrements and all queries re-run. - On failure, no invalidation occurs.
useSyncStatus
Observe the sync lifecycle.
const { isSyncing, lastSyncTime, queueDepth, error } = useSyncStatus();Return value
interface UseSyncStatusResult {
isSyncing: boolean;
lastSyncTime: Date | null;
queueDepth: number;
error: Error | null;
}isSyncing—trueduring a sync cycle.lastSyncTime— Timestamp of last successful sync, ornullif never synced.queueDepth— Number of pending deltas in the outbox queue.error— Last sync error. Cleared on next successful sync.
useAction
Execute imperative actions against external systems via the gateway. Wraps SyncCoordinator.executeAction() and subscribes to onActionComplete events.
const { execute, lastResult, isPending } = useAction();
await execute({
connector: "slack",
actionType: "send_message",
params: { channel: "#general", text: "Hello from LakeSync" },
});Return value
interface UseActionResult {
/** Execute an action against a connector via the gateway. */
execute: (params: ActionParams) => Promise<void>;
/** Last action result (success or error). Null before first execution. */
lastResult: ActionResult | ActionErrorResult | null;
/** Whether an action is currently in flight. */
isPending: boolean;
}
interface ActionParams {
connector: string;
actionType: string;
params: Record<string, unknown>;
idempotencyKey?: string;
}execute— queues the action and triggers immediate processing. The action flows through the gateway to the registeredActionHandler.lastResult— updated via theonActionCompleteevent. Check"data" in lastResultto distinguish success from error.isPending—truefrom the momentexecuteis called untilonActionCompletefires.idempotencyKey— optional deduplication key for at-most-once delivery.
useActionDiscovery
Discover available connectors and their supported action types. Use this to build dynamic UI — no hardcoded connector names.
const { connectors, isLoading, error, refetch } = useActionDiscovery();
for (const [name, actions] of Object.entries(connectors)) {
console.log(name, actions.map(a => a.actionType));
}Return value
interface UseActionDiscoveryResult {
/** Map of connector name to supported action descriptors. */
connectors: Record<string, ActionDescriptor[]>;
/** Whether discovery is loading. */
isLoading: boolean;
/** Error from the last fetch, or null. */
error: LakeSyncError | null;
/** Manually re-fetch available actions. */
refetch: () => void;
}connectors— empty object when no action handlers are registered or transport doesn't support discovery.- Fetches on mount; call
refetch()if handlers change at runtime. ActionDescriptorincludesactionType,description, and optionalparamsSchema.
Full Example
import { LocalDB, SyncCoordinator, HttpTransport } from "@lakesync/client";
import { MemoryActionQueue } from "@lakesync/client";
import {
LakeSyncProvider,
useQuery,
useMutation,
useSyncStatus,
useAction,
useActionDiscovery,
} from "@lakesync/react";
// Setup (once, outside React)
const db = (await LocalDB.open({ name: "app" })).value!;
const transport = new HttpTransport({
baseUrl: "https://gateway.example.com",
gatewayId: "my-gw",
token: "...",
});
const coordinator = new SyncCoordinator(db, transport, {
actionQueue: new MemoryActionQueue(),
});
coordinator.startAutoSync();
function App() {
return (
<LakeSyncProvider coordinator={coordinator}>
<Dashboard />
</LakeSyncProvider>
);
}
function Dashboard() {
const { data: todos, isLoading } = useQuery<Todo>("SELECT * FROM todos");
const { insert, remove } = useMutation();
const { queueDepth, error: syncError } = useSyncStatus();
const { execute, isPending } = useAction();
const { connectors } = useActionDiscovery();
if (isLoading) return <p>Loading...</p>;
return (
<div>
<header>
<span>{queueDepth} pending</span>
{syncError && <span>Sync error: {syncError.message}</span>}
</header>
<button onClick={() => insert("todos", crypto.randomUUID(), { text: "New", done: 0 })}>
Add Todo
</button>
<ul>
{todos.map(t => (
<li key={t._rowId}>
{t.text}
<button onClick={() => remove("todos", t._rowId)}>Delete</button>
</li>
))}
</ul>
{/* Dynamic action buttons from discovery */}
{Object.entries(connectors).map(([connector, actions]) =>
actions.map(action => (
<button
key={`${connector}:${action.actionType}`}
disabled={isPending}
onClick={() => execute({
connector,
actionType: action.actionType,
params: { /* action-specific params */ },
})}
>
{action.description}
</button>
))
)}
</div>
);
}Hooks Summary
| Hook | Purpose |
|---|---|
useLakeSync() | Raw SDK access (coordinator, tracker, invalidate) |
useQuery(sql, params?) | Reactive SQL queries with auto re-run |
useMutation() | Insert, update, remove with auto query invalidation |
useSyncStatus() | Sync lifecycle (syncing, queue depth, errors) |
useAction() | Execute imperative actions against connectors |
useActionDiscovery() | Discover available connectors and action types |