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/react

Frontend 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 dev auto-generates gencow/api.ts with typed defineProcedureQuery / defineProcedureMutation definitions. Never create this file manually.

Common mistake: Don't pass a string to useQuery — always use the api object: useQuery(api.tasks.list), not useQuery("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:

  • data
  • status
  • derivedStatus
  • currentStep
  • isActive
  • isTerminal
  • isLoading
  • isFetching
  • error
  • refetch

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 |

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:

  1. args is "skip"
  2. options.enabled is false
  3. No auth token available AND options.public is not true

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 managed

signUp

const user = await signUp("[email protected]", "password123", "John Doe");
// Same return as signIn — session is established immediately

signOut

await signOut();
// Clears JWT, sessionToken, and user from memory + localStorage

useAuth

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 load

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