React Hooks
@gencow/react — GencowProvider, useQuery, useMutation, createAuthClient
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.
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/react";
// createAuthClient() auto-reads VITE_API_URL from .env
export const auth = createAuthClient();
export const { signIn, signUp, signOut, useAuth } = auth;Step 2: Provider Wrapping
// src/main.tsx
import { GencowProvider } from "@gencow/react";
import { auth } from "./lib/auth";
function App() {
const baseUrl = import.meta.env.VITE_API_URL;
return (
<GencowProvider baseUrl={baseUrl} auth={auth}>
<YourApp />
</GencowProvider>
);
}Step 3: 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: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 }.
|---|---|
| baseUrl | string | Gencow server URL |
| auth | GencowAuthClient | Auth client returned by createAuthClient() |
| token | string \| null | Legacy JWT auth token prop |
| children | React.ReactNode | Child components |
Recommended Setup
import { GencowProvider } from "@gencow/react";
import { auth } from "./lib/auth";
function AppProvider({ children }: { children: React.ReactNode }) {
// VITE_API_URL is auto-set by `gencow init` and `gencow deploy`
const baseUrl = import.meta.env.VITE_API_URL;
return (
<GencowProvider baseUrl={baseUrl} auth={auth}>
{children}
</GencowProvider>
);
}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>
);
}createAuthClient
Create a complete auth client. Returns auth functions and hooks.
Signature
function createAuthClient(baseUrl?: string, options?: {
storagePrefix?: string;
sessionTokenStorage?: "localStorage" | "memory";
}): {
signIn: (email: string, password: string) => Promise<AuthUser>;
signUp: (email: string, password: string, name?: string) => Promise<AuthUser>;
signOut: () => Promise<void>;
useAuth: () => { token: string | null; user: AuthUser | null; isAuthenticated: boolean };
store: AuthStore; // Internal store (advanced use)
};Setup
// src/lib/auth.ts — one file, done
import { createAuthClient } from "@gencow/react";
export const { signIn, signUp, signOut, useAuth } = createAuthClient();
// XSS-sensitive surfaces can avoid refresh-token persistence:
// createAuthClient(undefined, { sessionTokenStorage: "memory" })signIn
const user = await signIn("[email protected]", "password123");
// user = { id: "abc123", email: "[email protected]", name: "John" }
// JWT + sessionToken automatically managedsignUp
const user = await signUp("[email protected]", "password123", "John Doe");
// Same return as signIn — session is established immediatelysignOut
await signOut();
// Clears JWT, sessionToken, and user from memory + localStorageuseAuth
function Profile() {
const { user, isAuthenticated, token } = useAuth();
if (!isAuthenticated) return <LoginPage />;
return <div>Welcome, {user.name}!</div>;
}Token Refresh Flow
JWT (5min) ← memory only, auto-refreshed 10sec before expiry
sessionToken (7d) ← localStorage by default, or memory with sessionTokenStorage
user ← localStorage, shown immediately on page loadProcedure 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 — procedure defs, auth store, realtime, procedure-rpc
- Core API — Backend API reference
- Authentication Guide — Auth setup walkthrough