SDK Hooks
React hooks for querying data, running mutations, and reading auth state.
Hooks are Client Component code. The package ships "use client" in its bundles, so importing it does not turn your file into a Client Component — the file that calls a hook still needs its own "use client".
useUser
Returns the current authenticated user, or null if signed out. isLoading is true during the initial session rehydration — use it to avoid flashing a logged-out UI on page reload.
import { useUser } from "@weirdscience/based-client";
function Profile() {
const { user, isLoading } = useUser();
if (isLoading) return <Spinner />;
if (!user) return <LoginForm />;
return <p>{user.email} ({user.role})</p>;
}| Field | Type | Description |
|---|---|---|
| user | { id, email, role } | null | Current user or null. role is "owner" | "member" |
| isLoading | boolean | True during session hydration on mount |
| error | HookError | null | Always null today — the hook reads client state, it never fetches |
useQuery
Fetch records from a table with optional filtering, ordering, and pagination. Pass your Tables type and a table name as generics for full type inference (see based typegen).
import type { Tables } from "./based";
import { useQuery } from "@weirdscience/based-client";
function PostList() {
const { data, total, isLoading, error, refetch } = useQuery<Tables, "posts">(
"posts",
{
filter: { status: "published" },
order: "createdAt.desc",
limit: 10,
offset: 0,
},
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>{error.code}: {error.message}</p>;
return (
<>
<p>{total} posts</p>
{data?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
<button onClick={refetch}>Refresh</button>
</>
);
}Options
| Option | Type | Description |
|---|---|---|
| filter | Partial<Record<keyof Row, string | number | boolean>> | Column filters as key-value pairs. An unknown column is a 400 VALIDATION_ERROR |
| order | `${keyof Row}.asc` | `${keyof Row}.desc` | Sort order. Defaults to id.asc so pagination stays stable |
| limit | number | Max rows to return. Defaults to 20, capped at 100 |
| offset | number | Number of rows to skip |
| enabled | boolean | When false the request never fires: data stays null and isLoading is false. Defaults to true |
Gate queries behind auth or any boolean to avoid a 403 while signed out:
const { user } = useUser();
const { data } = useQuery("notes", { enabled: !!user });
// Nothing is fetched until user is truthy.
// While disabled: data is null and isLoading is false.Queries refetch on their own when the signed-in user changes, so signing out clears the previous user's rows. Responses that arrive out of order are discarded — only the newest request can write to data.
Return value
| Field | Type | Description |
|---|---|---|
| data | Row[] | null | Rows, or null before the first response and while disabled |
| total | number | Total matching records (before pagination) |
| isLoading | boolean | True while fetching |
| error | HookError | null | { code, message, status?, details? } — not an Error instance |
| refetch | () => void | Re-run the query |
useRecord
Fetch a single record by id via GET /api/:table/:id. Unlike useQuery, data is a single row or null — not an array.
import type { Tables } from "./based";
import { useRecord } from "@weirdscience/based-client";
function Post({ id }: { id: string }) {
const { data: post, isLoading } = useRecord<Tables, "posts">("posts", id);
if (isLoading) return <Spinner />;
if (!post) return <NotFound />;
return <h1>{post.title}</h1>;
}Pass null or undefined as the id to skip the query — no need for a separate enabled flag:
const [selectedId, setSelectedId] = useState<string | null>(null);
const { data } = useRecord("posts", selectedId);
// No fetch until setSelectedId is called.Perfect for deterministic keys like ${userId}:${key} paired with PUT upsert:
const { data: prefs } = useRecord("preferences", `${user.id}:theme`);Return value
| Field | Type | Description |
|---|---|---|
| data | Row | null | The record, or null if not found or skipped |
| isLoading | boolean | True while fetching |
| error | HookError | null | { code, message, status?, details? } — not an Error instance |
| refetch | () => void | Re-run the query |
useMutation
Create, update, upsert, or delete records. The operation parameter picks the behavior. Pass <Tables, "tableName"> for typed mutation payloads.
mutate resolves to the row on success and to null on failure — it never throws, so an onClick handler cannot produce an unhandled rejection. Read the returned value, or the hook's error, instead of wrapping the call in try/catch.
import type { Tables } from "./based";
import { useMutation } from "@weirdscience/based-client";
function CreatePost() {
const { mutate, isLoading, error } = useMutation<Tables, "posts">("posts", "create");
async function onCreate() {
const created = await mutate({ title: "New Post", status: "draft" });
if (!created) return; // error holds { code, message, status?, details? }
console.log(created.id);
}
return (
<>
<button disabled={isLoading} onClick={onCreate}>Create</button>
{error && <p>{error.message}</p>}
</>
);
}
function UpdatePost({ id }: { id: string }) {
const { mutate } = useMutation<Tables, "posts">("posts", "update");
return (
<button onClick={() => mutate({ id, status: "published" })}>
Publish
</button>
);
}
function DeletePost({ id }: { id: string }) {
const { mutate } = useMutation<Tables, "posts">("posts", "delete");
return (
<button onClick={() => mutate({ id })}>
Delete
</button>
);
}
function SavePreference({ userId }: { userId: string }) {
// Upsert — creates if missing, updates if present. Perfect for
// client-chosen ids like `${userId}:theme`.
const { mutate } = useMutation<Tables, "preferences">("preferences", "upsert");
return (
<button onClick={() => mutate({ id: `${userId}:theme`, value: "dark" })}>
Use dark mode
</button>
);
}Operations
| Operation | mutate(data) | Notes |
|---|---|---|
| create | { ...fields } | POST. All required columns must be provided; unknown keys are a 400 |
| update | { id, ...fields } | PUT, so it upserts — creates if missing (201), updates if present (200) |
| upsert | { id, ...fields } | Same request as update; reads better when the caller picks the id |
| delete | { id } | DELETE. id is required |
Return value
| Field | Type | Description |
|---|---|---|
| mutate | (data) => Promise<Row | null> | Runs the write. Resolves to null on failure; never throws |
| isLoading | boolean | True while the mutation is in flight |
| error | HookError | null | { code, message, status?, details? } — not an Error instance |