React Hooks
@gencow/react — GencowProvider, useQuery, useMutation, useAuth
The @gencow/react package provides React-only hooks and provider. Typed query/mutation defs and auth core live in `@gencow/client`; codegen imports defs from there.
If your app already standardizes on @tanstack/react-query, use the separate `@gencow/tanstack-query` integration instead of mounting GencowProvider and GencowTanstackProvider together.
npm install @gencow/client @gencow/reactFrontend Quick Start
Set up a Gencow React frontend in 3 steps:
Step 1: Auth Client
// src/lib/auth.ts
import { createAuthClient } from "@gencow/client";
const baseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:5456";
export const auth = createAuthClient(baseUrl);
export const { signIn, signUp, signOut } = auth;Step 2: Runtime client
// src/lib/apiClient.ts
import { createGencowClient } from "@gencow/client";
import { api } from "../gencow/api";
import { auth } from "./auth";
const baseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:5456";
export const apiClient = createGencowClient({ api, baseUrl, auth });Step 3: Provider wrapping
// src/main.tsx
import { GencowProvider } from "@gencow/react";
import { apiClient } from "./lib/apiClient";
function App() {
return (
<GencowProvider apiClient={apiClient}>
<YourApp />
</GencowProvider>
);
}Step 4: Use Hooks
import { useQuery, useMutation } from "@gencow/react";
import { api } from "../gencow/api"; // auto-generated by `gencow dev`
function TaskList() {
// useQuery returns { data, isLoading, isFetching, error } — TanStack Query compatible
// createCrud().list returns { data: Task[], total: number }
const { data: result, isLoading, error } = useQuery(api.tasks.list, {});
// useMutation returns { mutate, isPending, error } — TanStack Query compatible
const { mutate: create, isPending: isCreating } = useMutation(api.tasks.create);
const { mutate: remove } = useMutation(api.tasks.remove);
if (isLoading && !result) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<p>{result?.total} tasks</p>
<button
onClick={() => create({ title: "New task" })}
disabled={isCreating}
>
{isCreating ? "Adding..." : "Add Task"}
</button>
<ul>
{result?.data.map((t) => (
<li key={t.id}>
{t.title}
<button onClick={() => remove({ id: t.id })}>🗑️</button>
</li>
))}
</ul>
</div>
);
}Key:
gencow devauto-generatesgencow/api.tswith typeddefineProcedureQuery/defineProcedureMutationdefinitions. Never create this file manually.
Common mistake: Don't pass a string to
useQuery— always use theapiobject and an args object for list queries:useQuery(api.tasks.list, {}), notuseQuery("tasks.list").
useWorkflow
Use useWorkflow() to track a workflow run by exact ID:
import { useWorkflow } from "@gencow/react";
import { api } from "../gencow/api";
const state = useWorkflow(api.workflows.get, run.id);Returned fields include:
datastatusderivedStatuscurrentStepisActiveisTerminalisLoadingisFetchingerrorrefetch
useWorkflow() prefers exact-id realtime updates and falls back to safe polling while the run is active.
Ownership rule:
useWorkflow(api.workflows.get, run.id)is the recommended way to inspect a run. If you add a custom workflow list query, keep that query authenticated instead of calling it with{ public: true }.
GencowProvider
Wrap your app root so hooks receive baseUrl, auth session state, and WebSocket context. Pass the runtime client from `createGencowClient`:
import { GencowProvider } from "@gencow/react";
import { apiClient } from "./lib/apiClient";
<GencowProvider apiClient={apiClient}>
{children}
</GencowProvider>apiClient bundles baseUrl, auth, generated api, and imperative call.* helpers. Hooks read session state from apiClient.auth via context — you do not pass token manually.
Props
| Prop | Type | Description |
|---|---|---|
apiClient |
GencowClient |
Preferred. From createGencowClient({ api, baseUrl, auth }) |
baseUrl |
string |
Deprecated. Use apiClient instead |
auth |
GencowAuthClientCore |
Deprecated. Use apiClient instead |
children |
React.ReactNode |
Child components |
If apiClient is set, it wins. The baseUrl + auth props remain for backwards compatibility during migration.
Recommended setup
// src/lib/apiClient.ts
import { createGencowClient } from "@gencow/client";
import { api } from "../gencow/api";
import { auth } from "./auth";
export const apiClient = createGencowClient({
api,
baseUrl: import.meta.env.VITE_API_URL,
auth,
});import { GencowProvider } from "@gencow/react";
import { apiClient } from "./lib/apiClient";
function AppProvider({ children }: { children: React.ReactNode }) {
return <GencowProvider apiClient={apiClient}>{children}</GencowProvider>;
}TanStack Query apps: use `GencowTanstackProvider` with
tanstackApionly — do not nestGencowProviderfor the same tree.
useQuery
Reactive data fetching. Returns { data, isLoading, isFetching, error } — TanStack Query compatible.
Signature
function useQuery<T>(
def: QueryDef<T>,
args?: Args | "skip",
options?: { enabled?: boolean; public?: boolean }
): { data: T | undefined; isLoading: boolean; isFetching: boolean; error: Error | null; refetch: () => Promise<void> };Parameters
| Parameter | Type | Description |
|---|---|---|
def |
QueryDef<T> |
Query definition from api.ts |
args |
object | "skip" |
Arguments for the query. Pass "skip" to disable |
options.enabled |
boolean |
When false, query is skipped (default: true) |
options.public |
boolean |
When true, query runs without auth token. Client-side only — server data access is controlled by ownerRls + createCrud() |
Return Value
| Field | Type | Description |
|---|---|---|
data |
T | undefined |
Last successful query result. undefined before the first result or while skipped |
isLoading |
boolean |
true only for the initial fetch when no data is available yet |
isFetching |
boolean |
true for any in-flight fetch, including background refetches with stale data |
error |
Error | null |
Error from last fetch attempt, or null |
refetch |
() => Promise<void> |
Manually re-fetch data. Rarely needed — WebSocket handles updates automatically |
Examples
import { useQuery } from "@gencow/react";
import { api } from "../gencow/api";
// Basic createCrud list query
// useQuery returns { data, isLoading, isFetching, error }
// createCrud().list returns { data: Task[], total: number }
const { data: result, isLoading, isFetching, error } = useQuery(api.tasks.list, {});
result?.data.map(t => ...) // access data array
result?.total // total count for pagination
// With arguments
const { data: task } = useQuery(api.tasks.getById, { id: 42 });
// Conditional: "skip" token
const messages = useQuery(
api.chat.getMessages,
conversationId ? { conversationId } : "skip"
);
// Conditional: TanStack Query-style enabled
const messages = useQuery(
api.chat.getMessages,
{ conversationId },
{ enabled: !!conversationId }
);
// Public query (calls API without JWT — for landing pages, etc.)
// Server must use createCrud(table, { allowAnonymous: true }) or procedure.query.allowAnonymous()
const publicPosts = useQuery(
api.posts.listPublic,
{},
{ public: true }
);Auto-Skip Behavior
useQuery automatically skips when:
argsis"skip"options.enabledisfalse- No auth
tokenavailable ANDoptions.publicis nottrue
useMutation
Execute mutations with pending state and error tracking. Returns { mutate, isPending, error } — TanStack Query compatible.
Signature
function useMutation<T>(
def: MutationDef<T>
): {
mutate: (args: Args) => Promise<Return>;
mutateAsync: (args: Args) => Promise<Return>; // alias for mutate
isPending: boolean;
error: Error | null;
};Return Value
| Field | Type | Description |
|---|---|---|
mutate |
(args) => Promise |
Mutation function |
mutateAsync |
(args) => Promise |
Same as mutate (TanStack compat alias) |
isPending |
boolean |
Whether mutation is in progress |
error |
Error | null |
Last error (or null) |
Examples
import { useMutation } from "@gencow/react";
import { api } from "../gencow/api";
function CreateTask() {
const { mutate: create, isPending, error } = useMutation(api.tasks.create);
return (
<div>
<button
onClick={() => create({ title: "New task" })}
disabled={isPending}
>
{isPending ? "Creating..." : "Create"}
</button>
{error && <p className="error">{error.message}</p>}
</div>
);
}File Upload (FormData)
const { mutate: upload, isPending } = useMutation(api.files.upload);
const handleUpload = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const result = await upload(formData);
// result = { storageId, url, name, size }
};useStatus
Monitor WebSocket connection health.
Signature
function useStatus(): { isConnected: boolean };Example
import { useStatus } from "@gencow/react";
function StatusBar() {
const { isConnected } = useStatus();
return (
<div className={isConnected ? "online" : "offline"}>
{isConnected ? "● Connected" : "○ Reconnecting..."}
</div>
);
}useAuth
Read auth state from the auth client inside your apiClient (via provider context). Import from @gencow/react — not from createAuthClient().
Create the client with `createAuthClient` from @gencow/client:
// src/lib/auth.ts
import { createAuthClient } from "@gencow/client";
const baseUrl = import.meta.env.VITE_API_URL ?? "http://localhost:5456";
// XSS-sensitive surfaces can avoid refresh-token persistence:
// createAuthClient(baseUrl, { strategy: { kind: "token", sessionTokenStorage: "memory" } })
export const auth = createAuthClient(baseUrl);
export const { signIn, signUp, signOut } = auth;
export { useAuth } from "@gencow/react";Signature
function useAuth(): {
user: AuthUser | null;
isAuthenticated: boolean;
/** @deprecated Prefer `user` and `isAuthenticated`. Avoid JWT in UI code. */
token: string | null;
};Usage
import { useAuth } from "@gencow/react";
// or re-export from src/lib/auth.ts
function Profile() {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) return <LoginPage />;
return <div>Welcome, {user?.name}!</div>;
}signIn, signUp, and signOut come from the auth client — see Client SDK — Auth.
Procedure defs (defineProcedureQuery / defineProcedureMutation)
Codegen emits these from @gencow/client into gencow/api.ts. See Client SDK.
Type Inference
Types flow end-to-end:
// Backend (gencow/tasks.ts) — createCrud or procedure.query
export const list = procedure.query
.name("tasks.list")
.handler(async ({ context: ctx }) => { ... });
// Auto-generated (gencow/api.ts)
import { defineProcedureQuery } from "@gencow/client";
import type * as Operations from "./operations.d.ts";
export const api = {
tasks: {
list: defineProcedureQuery<Operations.TasksListOutput>("tasks.list"),
},
};
// Frontend (component.tsx)
const { data, isLoading, isFetching, error } = useQuery(api.tasks.list, {});
// ^? { data: Task[], total: number } | undefined ← fully typed!Next Steps
- Client SDK —
createGencowClient, procedure defs, auth store - TanStack Query — alternative data stack (experimental)
- Core API — Backend API reference
- Authentication Guide — Auth setup walkthrough