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/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/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 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 and an args object for list queries: 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 }.


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.

// 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 tanstackApi only — do not nest GencowProvider for 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:

  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>
    );
}

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