LakeSync

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 react

Or via the unified package:

npm install lakesync react
import {
  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

ParameterTypeDescription
sqlstringSQL query string
paramsunknown[]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, or null.
  • isLoadingtrue on first render until the query resolves.
  • refetch — Manually trigger a re-run.

Reactivity

The query re-runs when any of these change:

  1. sql or params arguments
  2. dataVersion (incremented by remote sync via onChange)
  3. dataVersion (incremented by useMutation on successful writes)
  4. 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 (not delete) to avoid JS reserved word issues.
  • Each returns Result<void, LakeSyncError> — check .ok before proceeding.
  • On success, dataVersion increments 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;
}
  • isSyncingtrue during a sync cycle.
  • lastSyncTime — Timestamp of last successful sync, or null if 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 registered ActionHandler.
  • lastResult — updated via the onActionComplete event. Check "data" in lastResult to distinguish success from error.
  • isPendingtrue from the moment execute is called until onActionComplete fires.
  • 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.
  • ActionDescriptor includes actionType, description, and optional paramsSchema.

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

HookPurpose
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