TanStack Query

Use @gencow/tanstack-query for TanStack Query v5 apps with shared auth context and realtime cache sync

Experimental. @gencow/tanstack-query is published to npm, but the package surface, generated client shape, and examples may still change before a stable release.

Use this guide when your React app already standardizes on TanStack Query v5 and you want Gencow procedures to plug into that stack cleanly.

If you are starting a new frontend and do not already need TanStack Query features such as custom QueryClient policy, prefetch/hydration workflows, or broad existing hook usage, the default `@gencow/react` stack is still the simpler choice.

Choose One Data Stack

Gencow supports two frontend data stacks:

Stack Root provider Data hooks Best for
Default React SDK GencowProvider @gencow/react useQuery / useMutation Most new Gencow apps
TanStack Query GencowTanstackProvider @tanstack/react-query hooks + tanstackApi.*.queryOptions() Apps already built around TanStack Query

Do not use both stacks for the same procedures in the same tree. That creates duplicate caches, duplicate subscriptions, and confusing loading behavior.

What This Package Adds

Start with your generated api object from gencow codegen, then build:

  1. apiClient with `createGencowClient`
  2. tanstackApi with createTanstackQueryApiClient(apiClient)
  3. A root GencowTanstackProvider

The adapter gives each generated procedure a TanStack-friendly surface:

Procedure type Added methods
Query .queryOptions(), .infiniteOptions(), .queryKey(), .infiniteKey(), .subscriptionKey(), .call(), .key()
Mutation .mutationOptions(), .mutationKey(), .call(), .key()
Namespace .key() for partial-match invalidation

GencowTanstackProvider also mounts TanStack's QueryClientProvider and the shared Gencow auth/session context, then connects active queries to Gencow realtime so matching cache entries update or invalidate automatically.

Install

npm install @gencow/client @gencow/react @gencow/tanstack-query @tanstack/react-query react react-dom

Keep all @gencow/* packages on the same version.

Setup

1. Auth client

// src/lib/auth.ts
import { createAuthClient } from "@gencow/client";

export const auth = createAuthClient(import.meta.env.VITE_API_URL);

2. Runtime client, then TanStack adapter

// src/lib/apiClient.ts
import { createGencowClient } from "@gencow/client";
import { createTanstackQueryApiClient } from "@gencow/tanstack-query";
import { api } from "../gencow/api";
import { auth } from "./auth";

const baseUrl = import.meta.env.VITE_API_URL;

export const apiClient = createGencowClient({ api, baseUrl, auth });
export const tanstackApi = createTanstackQueryApiClient(apiClient);

createTanstackQueryApiClient takes an existing apiClient. There is no overload that accepts the raw api plus config directly.

3. Mount the TanStack provider

// src/main.tsx
import { GencowTanstackProvider } from "@gencow/tanstack-query";
import { tanstackApi } from "./lib/apiClient";

export function AppProvider({ children }: { children: React.ReactNode }) {
  return (
    <GencowTanstackProvider tanstackApi={tanstackApi}>
      {children}
    </GencowTanstackProvider>
  );
}

Do not nest GencowProvider around GencowTanstackProvider for the same app tree.

Query Pattern

Use TanStack hooks with options generated from tanstackApi:

import { useQuery } from "@tanstack/react-query";
import { tanstackApi } from "@/lib/apiClient";

function TaskList() {
  const { data, isLoading, error } = useQuery(
    tanstackApi.tasks.list.queryOptions({
      input: { status: "open" },
      staleTime: 60_000,
    }),
  );

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>{error.message}</p>;

  return (
    <ul>
      {data?.data.map((task) => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  );
}

For anonymous procedures generated from .allowAnonymous() or createCrud(..., { allowAnonymous: true }), the generated api defs already carry that default, so no extra flag is needed:

useQuery(tanstackApi.posts.listPublic.queryOptions());

You can still pass public: true explicitly when needed, but most app code should rely on the generated default instead.

If input is missing, use skipToken instead of manually building invalid args:

import { skipToken } from "@gencow/tanstack-query";

const query = useQuery(
  tanstackApi.tasks.get.queryOptions({
    input: selectedId ? { id: selectedId } : skipToken,
  }),
);

Mutation Pattern

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { tanstackApi } from "@/lib/apiClient";

function CreateTaskButton() {
  const queryClient = useQueryClient();
  const createTask = useMutation(
    tanstackApi.tasks.create.mutationOptions({
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: tanstackApi.tasks.key() });
      },
    }),
  );

  return (
    <button onClick={() => createTask.mutate({ title: "New task" })}>
      Add task
    </button>
  );
}

You can often rely on realtime invalidation instead of wiring manual refetches everywhere, but broad invalidation in onSuccess is still useful for flows with optimistic UI, background tabs, or custom cache grouping.

Realtime Behavior

GencowTanstackProvider watches the active TanStack cache and subscribes matching Gencow queries to realtime channels.

  • Public queries can receive pushed data directly.
  • Protected queries usually receive invalidate signals, then refetch over HTTP with auth.
  • Skipped queries do not subscribe.
  • Infinite queries subscribe by procedure path rather than per-page params.

That means you keep the standard TanStack mental model, but active cache entries stay synchronized with server-side procedure invalidation.

Imperative Calls

Outside React, call procedures directly:

const tasks = await tanstackApi.tasks.list.call({ status: "open" });

For server-side scripts or SSR, remember that protected procedures still need a usable auth context. If that environment cannot forward the user's token or session cookie, the call can still fail with 401.

Cache Keys

Use keys from tanstackApi, not from the raw generated api definitions:

queryClient.invalidateQueries({ queryKey: tanstackApi.key() });
queryClient.invalidateQueries({ queryKey: tanstackApi.tasks.key() });
queryClient.setQueryData(
  tanstackApi.tasks.get.queryKey({ input: { id: 123 } }),
  (old) => old ? { ...old, title: "Updated" } : old,
);

This is also how you keep invalidation aligned with the provider's realtime channel mapping.

Workflow Helper

The package also exports useWorkflow for workflow detail queries that should combine TanStack caching, realtime updates, and polling while a run is active:

import { useWorkflow } from "@gencow/tanstack-query";
import { tanstackApi } from "@/lib/apiClient";

const workflow = useWorkflow(tanstackApi.workflows.get, runId);

Common Mistakes

Mistake Result
Mixing @gencow/react hooks with TanStack hooks for the same procedure Two caches and duplicated realtime behavior
Nesting GencowProvider and GencowTanstackProvider Duplicate auth/session wiring
Creating tanstackApi from raw api instead of apiClient Missing required runtime config
Using raw api objects for cache keys Invalidation and realtime key mapping drift
Mixing raw/manual defs with generated expectations Anonymous defaults only auto-apply when the query def carries generated allowAnonymous metadata

API Reference

Use the API reference for the full exported surface and detailed examples: