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.
bun add @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 the generatedapiobject. Current CRUD list queries can omit args or pass an empty object:useQuery(api.tasks.list)/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 }.
useRealtimeChannel
Subscribe to an authorized custom realtime channel. The backend must declare the
same channel with defineRealtimeChannels() and authorize the current user.
import { useRealtimeChannel } from "@gencow/react";
useRealtimeChannel(
{
channel: "conversation.events",
args: { workspaceId, conversationId },
},
(event) => {
console.log(event);
},
{ enabled: Boolean(conversationId) },
);useRealtimeChannel(
request: { channel: string; args?: unknown } | null | undefined,
listener: (data?: unknown) => void,
options?: { enabled?: boolean },
): voidPass null or undefined, or set enabled: false, when the subscription is not
ready. The hook updates the listener without reconnecting and resubscribes when
the session, channel, or serialized args change.
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;
allowAnonymous?: boolean;
public?: boolean;
realtime?: "public" | "authenticated" | "authorized" | false;
}
): { 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.allowAnonymous |
boolean |
Override the generated anonymous-call default. Usually unnecessary because codegen mirrors server allowAnonymous. |
options.public |
boolean |
Legacy alias for anonymous calls. Prefer server allowAnonymous plus generated defaults. |
options.realtime |
"public" | "authenticated" | "authorized" | false |
Override the generated realtime subscription policy. Use false to opt a hook out of realtime. |
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 when codegen carries allowAnonymous)
// Server must use createCrud(table, { allowAnonymous: true }) or procedure.query.allowAnonymous()
const publicPosts = useQuery(api.posts.listPublic, {});
// Per-hook opt-out from realtime
const staticSnapshot = useQuery(api.tasks.list, {}, { realtime: false });Auto-Skip Behavior
useQuery automatically skips when:
argsis"skip"options.enabledisfalse- No auth
tokenis available and neither the generated def nor hook options allow anonymous calls
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", { realtime: "authorized" }),
},
};
// 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