Loomup Docs
Guide /docs/sdk-react

React SDK

Package: @loomup/react (SDK repository path: packages/react).

Thin React bindings on top of @loomup/client: provider, auth session, REST query hooks, and realtime live queries.

Install

bash
# from a project that can reach the monorepo paths
npm install ../path/to/packages/client ../path/to/packages/react
# or after publish:
# npm install @loomup/client @loomup/react

Peer dependencies: react ≥ 18, @loomup/client ≥ 0.1.0.

Quick start

tsx
import { createClient } from "@loomup/client";
import {
  LoomupProvider,
  useAuth,
  useLiveQuery,
  useMutation,
} from "@loomup/react";

// Keep the client stable (module scope or useMemo once).
const client = createClient({ url: "http://127.0.0.1:3000" });

function App() {
  return (
    <LoomupProvider client={client} persist={{ enabled: true }}>
      <Todos />
    </LoomupProvider>
  );
}

function Todos() {
  const { user, loading: authLoading, signIn, signUp, signOut } = useAuth();
  const { data, loading, ready } = useLiveQuery("todos", {
    sort: "-id",
    limit: 50,
    // "refetch" (default) is safest with where/rules;
    // "merge" applies INSERT/UPDATE/DELETE/RESYNC to local state.
    strategy: "merge",
  });
  const { mutate: addTodo } = useMutation((c, title: string) =>
    c.from("todos").insert({ title, completed: 0 }),
  );

  if (authLoading) return <p>Loading…</p>;
  if (!user) {
    return (
      <button
        type="button"
        onClick={() => signIn({ email: "a@b.com", password: "secret12" })}
      >
        Sign in
      </button>
    );
  }

  return (
    <div>
      <p>
        {user.email}{" "}
        <button type="button" onClick={() => signOut()}>
          Sign out
        </button>
      </p>
      <button type="button" onClick={() => addTodo("Ship React SDK")}>
        Add
      </button>
      {loading && <p>Loading todos…</p>}
      <ul>
        {(data ?? []).map((row) => (
          <li key={String(row.id)}>{String(row.title)}</li>
        ))}
      </ul>
      {ready && <small>Live</small>}
    </div>
  );
}

You can also pass options={{ url }} instead of client and the provider will create a client once on mount.

API

LoomupProvider

PropDescription
clientExisting LoomupClient (preferred).
optionsCreateClientOptions when client is omitted.
persist.enabledStore access/refresh tokens via persist.storage (default backend: localStorage).
persist.storageKeyKey prefix (default loomup).
persist.storageOptional TokenStorage adapter (sync or async).

TokenStorage

Pluggable backend for session tokens (used when persist.enabled is true):

ts
type TokenStorage = {
  getItem(key: string): string | null | Promise<string | null>;
  setItem(key: string, value: string): void | Promise<void>;
  removeItem(key: string): void | Promise<void>;
};

Default: browser localStorage. For React Native, use @loomup/react-native (AsyncStorage) or pass a custom adapter (e.g. SecureStore).

useLoomup()

Returns the client from context. Throws outside the provider.

useAuth()

FieldDescription
userCurrent user or null.
session{ accessToken, refreshToken }.
loading / errorAsync state.
signIn / signUp / signOut / refresh / meSession methods.

On mount, if the client already has an access token (or one is restored from storage), calls auth.me().

useSelect(table, opts?) / useRow(table, id, opts?)

REST list / get with loading, error, refetch. Options: where, sort, limit, offset, enabled.

useMutation(fn)

ts
const { mutate, loading, error, reset } = useMutation((client, ...args) =>
  client.from("todos").insert(...),
);

useSubscribe(table, handler, opts?)

Subscribes on mount; unsubscribes on unmount. Uses subscribeReady by default (waitForAck: true). Handler identity can change without resubscribing.

useLiveQuery(table, opts?)

Initial select + realtime subscription.

strategyBehavior
refetch (default)Re-run select on every change (correct with filters/rules).
mergeApply INSERT/UPDATE/DELETE/RESYNC into the local array.

Filtered lists cannot perfectly re-evaluate server where/rules client-side; prefer refetch when filters matter.

Types

Use generated maps from the core SDK:

bash
loomup gen typescript --config loomup.toml --output ./loomup-types.ts
ts
import { createClient } from "@loomup/client";
import type { TableMap, TableInsertMap, TableUpdateMap } from "./loomup-types";

const client = createClient<TableMap, TableInsertMap, TableUpdateMap>({
  url: "http://127.0.0.1:3000",
});

@loomup/react re-exports createClient, LoomupError, and common types for convenience.

Example

bash
cd examples/todo-react && npm install && npm run dev
# against a running loomup serve on :3000

Publish workflow (maintainers)

  1. Publish or link @loomup/client first.
  2. Bump packages/react/package.json version.
  3. npm test / npm run build in packages/react.
  4. npm publish --access public.

React Native

Use @loomup/react-native for AsyncStorage-backed sessions and RN-oriented defaults. The hooks in this package work on React Native once token storage is provided via persist.storage.

Offline resource sync

Use SyncStoreProvider and useSyncResource for persistent local rows, optimistic offline mutations, status, and explicit conflicts. See Offline sync v1 for setup and recovery behavior.

Non-goals (v1)

  • Next.js SSR / cookie-mode helpers (browser / client components first).
  • React Query / SWR adapters (wrap the promise APIs if needed).
  • Automatic field-level conflict merging or generic CRDTs.