TanStack Query
Use @gencow/tanstack-query for TanStack Query v5 apps with shared auth context and realtime cache sync
Experimental.
@gencow/tanstack-queryis 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:
apiClientwith `createGencowClient`tanstackApiwithcreateTanstackQueryApiClient(apiClient)- 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-domKeep 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: