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
# 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
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
| Prop | Description |
|---|---|
client | Existing LoomupClient (preferred). |
options | CreateClientOptions when client is omitted. |
persist.enabled | Store access/refresh tokens via persist.storage (default backend: localStorage). |
persist.storageKey | Key prefix (default loomup). |
persist.storage | Optional TokenStorage adapter (sync or async). |
TokenStorage
Pluggable backend for session tokens (used when persist.enabled is true):
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()
| Field | Description |
|---|---|
user | Current user or null. |
session | { accessToken, refreshToken }. |
loading / error | Async state. |
signIn / signUp / signOut / refresh / me | Session 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)
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.
strategy | Behavior |
|---|---|
refetch (default) | Re-run select on every change (correct with filters/rules). |
merge | Apply 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:
loomup gen typescript --config loomup.toml --output ./loomup-types.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
cd examples/todo-react && npm install && npm run dev
# against a running loomup serve on :3000
Publish workflow (maintainers)
- Publish or link
@loomup/clientfirst. - Bump
packages/react/package.jsonversion. npm test/npm run buildinpackages/react.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.