npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gencow/tanstack-query

v0.2.4

Published

TanStack Query adapter for Gencow — queryOptions, mutationOptions, realtime cache sync

Readme

@gencow/tanstack-query

Experimental. APIs, codegen output, and integration guidance may change before a non-experimental release.

TanStack Query adapter for Gencow. It wraps your generated api object (from gencow codegen) into type-safe .queryOptions(), .mutationOptions(), and hierarchical cache keys for invalidation, prefetch, and cache updates.

This package assumes you already know TanStack Query. If you are new to it, read the official docs first.

When to use this package

| Approach | Packages | Best for | |----------|----------|----------| | Built-in React hooks (default) | @gencow/client + @gencow/react | Most React apps — useQuery / useMutation with realtime out of the box | | TanStack Query (this package) | @gencow/client + @gencow/react + @gencow/tanstack-query + @tanstack/react-query | Apps already standardized on TanStack Query (useQuery, useInfiniteQuery, QueryClient, prefetch, suspense, etc.) |

Do not mix both stacks for the same data. Pick one of @gencow/react hooks or TanStack hooks backed by this adapter — not both for the same queries.

Prerequisites

  1. A Gencow project with gencow codegen (or gencow dev) so you have a generated gencow/api.ts that exports api.
  2. An auth client from @gencow/client (createAuthClient).
  3. React 18+ and TanStack Query v5.

Install @gencow/react as a peer — TanStack apps use one root provider (GencowTanstackProvider) that shares the same React context as built-in hooks. Do not nest <GencowProvider> for the same tree.

Installation

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

Peer dependencies: @gencow/react, @tanstack/react-query (v5+), react (v18+).

Setup

1. Auth client

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

export const auth = createAuthClient(import.meta.env.VITE_API_URL);
// Optional: token strategy with in-memory session token for XSS-sensitive apps
// createAuthClient(url, { strategy: { kind: "token", sessionTokenStorage: "memory" } })

2. Runtime + TanStack API clients

Create the framework-agnostic runtime client first, then the TanStack adapter (separate types and factories):

// lib/gencow.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 mirrors the shape of api: every query becomes an operation with .queryOptions(), .infiniteOptions(), .call(), keys, etc.; every mutation gets .mutationOptions() and .call(). Use apiClient.call.* for imperative/SSR calls outside React.

3. Provider (React)

GencowTanstackProvider supplies:

  • QueryClientProvider (creates a default QueryClient with staleTime: 60_000 unless you pass your own)
  • Shared GencowContext from @gencow/react (useAuth, useGencowCtx, useRealtimeChannel work here)
  • Automatic realtime cache sync — WebSocket subscriptions for active queries, updating or invalidating cache entries when the server pushes changes
// app/providers.tsx
"use client";

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

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

Pass a custom queryClient when you need shared prefetch/hydration setup:

import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient({ /* your defaults */ });

<GencowTanstackProvider tanstackApi={tanstackApi} queryClient={queryClient}>
  {children}
</GencowTanstackProvider>

4. Use TanStack hooks in components

"use client";

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

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

  const create = useMutation(apiClient.tasks.create.mutationOptions());

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>Error</p>;

  return (
    <ul>
      {data?.map((task) => (
        <li key={task.id}>{task.title}</li>
      ))}
      <button onClick={() => create.mutate({ title: "New task" })}>Add</button>
    </ul>
  );
}

Generated api object

gencow codegen emits api with query and mutation definitions (from @gencow/client). Do not hand-write those defs in application code.

Your app should:

  1. Import api from the generated file.
  2. Create apiClient with createGencowClient, then tanstackApi with createTanstackQueryApiClient(apiClient).
  3. Use tanstackApi with TanStack hooks; use apiClient.call.* for imperative/SSR calls.

Query options

Use .queryOptions() with useQuery, useSuspenseQuery, or queryClient.prefetchQuery.

useQuery(
  apiClient.tasks.get.queryOptions({
    input: { id: 123 },
    staleTime: 60_000,
    retry: 2,
  }),
);

queryKey and queryFn are set for you from the Gencow definition. You can pass any other UseQueryOptions field (staleTime, enabled, select, etc.).

Public (unauthenticated) queries

Protected queries throw if the user is not signed in. For endpoints that allow anonymous access (as declared in your Gencow schema / codegen), pass public: true:

apiClient.health.check.queryOptions({ public: true });

Infinite query options

Use .infiniteOptions() with useInfiniteQuery or prefetchInfiniteQuery.

The input field must be a function that receives the page parameter and returns the procedure input:

useInfiniteQuery(
  apiClient.tasks.list.infiniteOptions({
    input: (offset: number | undefined) => ({ limit: 10, offset }),
    initialPageParam: undefined,
    getNextPageParam: (lastPage) => lastPage.nextOffset,
  }),
);

Mutation options

Use .mutationOptions() with useMutation:

const mutation = useMutation(
  apiClient.tasks.create.mutationOptions({
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: apiClient.tasks.key() });
    },
  }),
);

mutation.mutate({ title: "Earth" });

Calling procedures directly

Use tanstackApi.*.call() or apiClient.call.query / apiClient.call.mutate without React — useful in loaders, scripts, or server code. No provider is required for imperative calls.

const tasks = await tanstackApi.tasks.list.call({ status: "open" });
// or: await apiClient.call.query(api.tasks.list, { status: "open" });

Query and mutation keys

Keys use a stable tuple shape: a path array plus an optional params object. Use methods on apiClient, not on the raw api defs.

| Method | Purpose | |--------|---------| | .key() | Partial-match prefix (invalidate broad groups) | | .queryKey({ input }) | Full key for a standard query | | .infiniteKey({ input, initialPageParam }) | Full key for an infinite query | | .mutationKey() | Full key for a mutation | | .subscriptionKey({ input, type? }) | WebSocket channel string for realtime, or null if not subscribable |

Examples:

const queryClient = useQueryClient();

// Invalidate all Gencow-backed queries
queryClient.invalidateQueries({ queryKey: apiClient.key() });

// Invalidate only queries under the `tasks` namespace
queryClient.invalidateQueries({ queryKey: apiClient.tasks.key() });

// Invalidate only regular (non-infinite) task list queries
queryClient.invalidateQueries({
  queryKey: apiClient.tasks.list.key({ type: "query" }),
});

// Invalidate a specific list query
queryClient.invalidateQueries({
  queryKey: apiClient.tasks.list.key({ type: "query", input: { status: "open" } }),
});

// Update cache for one query
queryClient.setQueryData(
  apiClient.tasks.get.queryKey({ input: { id: 123 } }),
  (old) => ({ ...old, title: "Updated" }),
);

Default key prefix is ["gencow"]. Override when you need separate clients in one app:

const adminRuntime = createGencowClient({ api: adminApi, baseUrl, auth });
const adminTanstackApi = createTanstackQueryApiClient(adminRuntime, {
  path: ["gencow", "admin"],
});

Key shape (for debugging)

Standard query:

[["gencow", "tasks", "list", "query"], { "input": { "status": "open" } }]

Infinite query (seed from first page input):

[["gencow", "tasks", "list", "infinite"], { "seed": { "offset": 0 } }]

Mutation:

[["gencow", "tasks", "create", "mutation"]]

Partial-match namespace:

[["gencow", "tasks"]]

Conditionally disabling queries (skipToken)

Use skipToken instead of enabled: false when the query should not run because input is missing — it keeps types precise and sets enabled: false automatically.

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

const search = "...";

useQuery(
  apiClient.tasks.list.queryOptions({
    input: search ? { search } : skipToken,
  }),
);

useInfiniteQuery(
  apiClient.tasks.list.infiniteOptions({
    input: search
      ? (offset) => ({ limit: 10, offset, search })
      : skipToken,
    initialPageParam: undefined,
    getNextPageParam: (page) => page.nextOffset,
  }),
);

Skipped queries use { skip: true } in the key params segment and do not subscribe to realtime.

Realtime (WebSocket cache sync)

When GencowTanstackProvider is mounted, it watches the TanStack query cache. For each active query whose key maps to a subscription channel:

  • Server push with dataqueryClient.setQueryData(queryKey, pushed)
  • Server push without data (invalidate signal) → queryClient.invalidateQueries({ queryKey })

Subscription channels are derived automatically from each query's cache key.

  • Standard queries: channel includes serialized input (e.g. tasks.list::{"status":"open"}).
  • Infinite queries: channel is the operation name only (e.g. tasks.list) — page args are not part of the channel.

Inspect or test channels explicitly:

apiClient.tasks.list.subscriptionKey({ input: { status: "open" } });
// → "tasks.list::{\"status\":\"open\"}" or similar

apiClient.subscriptionKeyFromQueryKey(someQueryKeyFromCache);

Realtime requires a signed-in user (same as protected .call()). Use public: true only where your API allows anonymous reads.

Session hooks (useAuth, useGencowCtx)

Inside GencowTanstackProvider, import from @gencow/tanstack-query or @gencow/react:

import { useAuth, useGencowCtx } from "@gencow/tanstack-query";

const { user, isAuthenticated } = useAuth();
const { baseUrl, authStateKey } = useGencowCtx();

Also re-exported: useRealtimeChannel, useWorkflow (TanStack variant), GencowClientCtx.

Prefetch and suspense

// Server or client prefetch
await queryClient.prefetchQuery(
  apiClient.tasks.list.queryOptions({ input: { status: "open" } }),
);

// Suspense
const { data } = useSuspenseQuery(
  apiClient.tasks.list.queryOptions({ input: { status: "open" } }),
);

Use your own dehydrate/hydrate setup if you SSR — this package does not ship a custom serializer; follow TanStack Query hydration for your framework.

Exports

| Export | Description | |--------|-------------| | createTanstackQueryApiClient | GencowClientGencowTanstackApiClient | | GencowTanstackProvider | Single root provider (TanStack + shared Gencow context) | | useAuth, useGencowCtx, useRealtimeChannel | Re-exported from @gencow/react | | useWorkflow | TanStack workflow polling + realtime | | skipToken | Type-safe “no input” sentinel | | Types | GencowTanstackApiClient, GencowQueryOptions, GencowMutationOptions, etc. |

Troubleshooting

| Symptom | Likely cause | |---------|----------------| | requires authentication on .call() / fetch | User not signed in; use public: true only for anonymous endpoints | | Realtime not updating | Query has no observers, key is skipped, or provider not mounted | | Duplicate fetches / odd cache sharing | Two apiClient instances with the same path and inputs — use distinct path prefixes | | Mixed loading behavior | Using both @gencow/react useQuery and TanStack useQuery on the same procedure |

See also