Katman
Integrations

TanStack Query

Generate type-safe queryOptions, mutationOptions, and cache keys from your Katman client.

TanStack Query is the most popular data-fetching library for React, Vue, Solid, and Svelte. Katman provides createQueryUtils, a utility that generates type-safe queryOptions and mutationOptions from your client — so you don't have to write query keys or fetch functions by hand.

Prerequisites

You need a Katman client set up. The TanStack Query integration wraps this client.

npm install @tanstack/react-query

(Or @tanstack/vue-query, @tanstack/solid-query, @tanstack/svelte-query — the API is the same.)

Setup

Create query utils

import {  } from "katman/tanstack-query"
import {  } from "./client" // your Katman client

const  = ()

The utils object mirrors your client's structure. If your client has client.users.list, then utils.users.list gives you query/mutation helpers for that procedure.

Use in components

import { useQuery } from "@tanstack/react-query"

function UserList() {
  const { data, isLoading } = useQuery(
    utils.users.list.queryOptions({ input: { limit: 10 } })
  )

  if (isLoading) return <p>Loading...</p>
  return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

Queries

queryOptions returns everything useQuery needs — the query key, the fetch function, and any options you pass:

const  = utils.users.list.queryOptions({
  : { : 10 },
  : 5000,        // data is fresh for 5 seconds
  : 60_000,         // garbage collect after 1 minute
  : 30_000, // refetch every 30 seconds
  : isLoggedIn,     // only fetch when logged in
})

// options contains:
// - queryKey: [["users", "list"], { type: "query", input: { limit: 10 } }]
// - queryFn: ({ signal }) => client.users.list({ limit: 10 }, { signal })
// - staleTime, gcTime, etc.

Use with useQuery:

const { data } = useQuery(utils.users.list.queryOptions({ input: { limit: 10 } }))

Or prefetch on the server:

await queryClient.prefetchQuery(
  utils.users.list.queryOptions({ : { : 10 } })
)

Mutations

mutationOptions returns everything useMutation needs:

import { useMutation, useQueryClient } from "@tanstack/react-query"

function CreateUser() {
  const queryClient = useQueryClient()

  const { mutate, isPending } = useMutation(
    utils.users.create.mutationOptions({
      onSuccess: () => {
        // Invalidate the user list so it refetches
        queryClient.invalidateQueries({
          queryKey: utils.users.key(),
        })
      },
    })
  )

  return (
    <button onClick={() => mutate({ name: "Alice" })} disabled={isPending}>
      {isPending ? "Creating..." : "Create User"}
    </button>
  )
}

Cache invalidation

Every level of the utils tree has a key() method for generating cache keys:

// Invalidate all queries under "users" (list, create, etc.)
queryClient.invalidateQueries({
  : utils.users.key(),
})

// Invalidate a specific query with specific input
queryClient.invalidateQueries({
  : utils.users.list.queryKey({ : 10 }),
})

// Invalidate everything in the entire app
queryClient.invalidateQueries({
  : utils.key(),
})

The key structure is [path, { type, input }], which means TanStack Query's prefix matching works naturally. Invalidating utils.users.key() clears both users.list and users.create queries.

Direct calls

If you need to call a procedure outside of React (in a loader, a utility function, etc.), each procedure util has a call method:

const  = await utils.users.list.call(
  { : 10 },
  { : controller.signal },
)

This is the same as calling the client directly, but it's convenient when you're already working with the utils.

Available methods

Each procedure-level util provides:

MethodReturnsDescription
queryOptions(opts){ queryKey, queryFn, ... }Full options for useQuery
infiniteOptions(opts){ queryKey, queryFn, ... }Full options for useInfiniteQuery
mutationOptions(opts?){ mutationKey, mutationFn, ... }Full options for useMutation
queryKey(input?)[path, { type, input }]Query key for cache operations
mutationKey()[path, { type }]Mutation key for cache operations
call(input, opts?)Promise<TOutput>Direct procedure call

Each router-level util provides:

MethodReturnsDescription
key(input?)[path, options?]Key prefix for bulk invalidation

This works with @tanstack/react-query, @tanstack/vue-query, @tanstack/solid-query, and @tanstack/svelte-query. Same API, same types, different framework bindings.

Infinite queries

Use infiniteOptions for paginated data. It works like queryOptions but returns everything useInfiniteQuery needs:

import { useInfiniteQuery } from "@tanstack/react-query"

function UserList() {
  const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
    utils.users.list.infiniteOptions({
      input: (pageParam) => ({ cursor: pageParam, limit: 10 }),
      initialPageParam: 0,
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    })
  )

  return (
    <>
      {data?.pages.flatMap(p => p.items).map(u => <p key={u.id}>{u.name}</p>)}
      {hasNextPage && <button onClick={() => fetchNextPage()}>Load more</button>}
    </>
  )
}

The input option is a function that receives the current page parameter and returns the procedure input. The query key includes the base input so cache invalidation works the same way as regular queries.

skipToken

Use skipToken to conditionally disable a query. This is the type-safe alternative to enabled: false — it tells both TanStack Query and TypeScript that the query should not run:

import { skipToken } from "katman/tanstack-query"
import { useQuery } from "@tanstack/react-query"

function UserProfile({ userId }: { userId: string | null }) {
  // Query is disabled until userId is available
  const { data } = useQuery(
    utils.users.get.queryOptions({
      input: userId ? { id: userId } : skipToken,
    })
  )

  return data ? <p>{data.name}</p> : <p>Select a user</p>
}

When input is skipToken, the query key is still generated (for prefetching later) but queryFn is never called.

Streamed queries

For subscription-like data that accumulates over time, streamedOptions collects all events into an array:

function EventLog() {
  const { data: events } = useQuery(
    utils.stream.updates.streamedOptions({
      input: {},
    })
  )

  return (
    <ul>
      {events?.map((e, i) => <li key={i}>{JSON.stringify(e)}</li>)}
    </ul>
  )
}

Each SSE event is appended to the array. The query resolves when the stream ends.

Live queries

liveOptions refetches at an interval — the latest result replaces the previous. Useful for dashboards and status pages:

function ServerStatus() {
  const { data } = useQuery(
    utils.health.liveOptions({
      input: undefined,
      refetchInterval: 5000, // poll every 5 seconds
    })
  )

  return <p>Uptime: {data?.uptime}s</p>
}

What's next?

  • Client — set up the Katman client that powers the query utils
  • React Server Actions — an alternative for server-first React apps
  • Typed Errors — handle errors from your queries and mutations

On this page