# use-q > An opinionated, schema-driven way to use TanStack Query v5 — TkDodo's Practical React Query patterns as a typed API client. Complete documentation. Index: https://use-q.dev/llms.txt --- # Introduction > use-q is an opinionated, schema-driven way to use TanStack Query v5 — TkDodo's Practical React Query patterns as a typed API client. `use-q` is an opinionated way to use [TanStack Query](https://tanstack.com/query/latest) v5. The opinions are Dominik Dorfmeister ([TkDodo](https://tkdodo.eu))'s, from the [Practical React Query](https://tkdodo.eu/blog/practical-react-query) series — encoded as a thin, schema-driven API client. You describe your API once as a plain object of `RouteDefinition`s. From that single source of truth, `use-q` infers path params, search params, request bodies, and response shapes, builds hierarchical query keys, and wires up cache invalidation automatically through schema-defined tags. No `any` leaks into your components, and no query keys are written by hand. You still use TanStack Query. `use-q` does not replace it or hide the `QueryClient` — it makes the recommended patterns the path of least resistance. See [Practical React Query](https://use-q.dev/docs/getting-started/practical-react-query.md) for how each API maps to TkDodo's posts. ## The packages | Package | What it provides | | --- | --- | | `@use-q/api-client` | Framework-agnostic core: `createFetcher`, the `Schema` / `RouteDefinition` types, and error helpers. Zero runtime dependencies. | | `@use-q/api-client-react` | `createApiClient` and the React hooks (`useQ`, `useM`, `useInfiniteQ`, `useSuspenseQ`, `useQClient`) on top of TanStack Query v5. | | `@use-q/api-client-codegen` | Generates a typed schema from an OpenAPI 3.x spec. Ships the `use-q-codegen` CLI bin. | The core runs anywhere `fetch` does — Node, Bun, Deno, edge workers, or the browser — so you can share one schema between server actions, loaders, scripts, and your React app. ## A quick taste Define a schema and create a client: ```ts import { createApiClient } from "@use-q/api-client-react"; import type { RouteDefinition } from "@use-q/api-client"; interface Post { id: string; title: string; } const schema = { listPosts: { method: "GET", path: "/facilities/{facilityId}/posts", tags: ["posts"], } satisfies RouteDefinition<{ facilityId: string }, never, never, Post[]>, createPost: { method: "POST", path: "/facilities/{facilityId}/posts", invalidatesTags: ["posts"], } satisfies RouteDefinition<{ facilityId: string }, never, { title: string }, Post>, } as const; export const api = createApiClient(schema, { baseUrl: "https://api.example.com", }); export const { useQ, useM, useQClient } = api; ``` Then use it in a component: ```tsx function PostList({ facilityId }: { facilityId: string }) { const { data, isLoading } = api.useQ("listPosts", { params: { facilityId } }); const createPost = api.useM("createPost"); if (isLoading) return

Loading…

; return ( <> {data?.map((p) =>
{p.title}
)} ); } ``` That's it — the list refetches automatically after the mutation, because `createPost.invalidatesTags` matches `listPosts.tags`. ## Design principles These are TkDodo's Practical React Query defaults, as a library: - **End-to-end type safety.** Path params, search params, request bodies, and response shapes are all inferred from a single `RouteDefinition` map — not from `useQuery` generics. - **A query abstraction, not a new mental model.** `createApiClient` is the colocated `queryKey` + `queryFn` layer. You still mount `QueryClientProvider` and pass `staleTime` / `select` / `enabled`. - **Hierarchical query keys you never type twice.** Keys are always `["api", method, path, searchParams]`. Prefix invalidation and a `queryKeys` factory come for free. - **Schema-driven invalidation.** Tag routes with `tags` / `invalidatesTags` and mutations refresh the right queries in `onSettled` through a shared `TagRegistry`. - **Optimistic updates that compose.** Update multiple caches per mutation with snapshot/rollback semantics built in. Combine with `additionalInvalidatesTags` for arbitrary fan-out. - **Framework-agnostic core.** `@use-q/api-client` is a tiny zero-dependency fetcher that runs in Node, Bun, Deno, edge workers, or the browser. Use it from server actions, loaders, or scripts — no React required. - **OpenAPI codegen.** Generate a typed schema directly from an OpenAPI 3.x spec with `use-q-codegen` from `@use-q/api-client-codegen`. Pagination is detected automatically. - **Bring your own QueryClient.** Reuse a single `QueryClient` across multiple `createApiClient` instances, or hook into persistence and devtools — `use-q` never gets in the way. ## Where to next? - [Practical React Query](https://use-q.dev/docs/getting-started/practical-react-query.md): The opinions — and links to TkDodo's series - [Install](https://use-q.dev/docs/getting-started/installation.md): Install `@use-q/api-client` and the React layer - [5-minute quick start](https://use-q.dev/docs/getting-started/quick-start.md): Build a typed list + create flow - [Define a schema](https://use-q.dev/docs/getting-started/schema-definition.md): Anatomy of a `RouteDefinition` ## LLM-readable docs Agents can read these docs at [`/llms.txt`](https://use-q.dev/llms.txt) (an index of every page) or [`/llms-full.txt`](https://use-q.dev/llms-full.txt) (the complete corpus in one file). Each page is also available as Markdown by appending `.md` to its URL. --- # Practical React Query > use-q is an opinionated way to use TanStack Query — TkDodo's Practical React Query series, encoded as a schema-driven client. `use-q` is not a new data-fetching library. It is an **opinionated way to use [TanStack Query](https://tanstack.com/query/latest)**. The opinions come from Dominik Dorfmeister ([TkDodo](https://tkdodo.eu))'s [Practical React Query](https://tkdodo.eu/blog/practical-react-query) series: treat the cache as a server-state manager, never hand-write query keys, colocate a query abstraction, invalidate by describing relationships, transform with `select`, and keep TypeScript inference flowing from the query function. You still mount a `QueryClientProvider`. You still pass `staleTime`, `select`, and `enabled`. `use-q` makes those recommended patterns the path of least resistance so every app doesn't reinvent a query-key factory and a mutation-invalidation graph. > **Info:** > This page is a map, not a restatement of the series. When you want the _why_ behind a default, follow the link to TkDodo. When you want the _how_ in this library, follow the use-q guide next to it. ## What use-q bakes in ### A query abstraction, not a new mental model [Creating Query Abstractions](https://tkdodo.eu/blog/creating-query-abstractions) and [The Query Options API](https://tkdodo.eu/blog/the-query-options-api) argue for a single place that owns `queryKey` + `queryFn` (and the types). [`createApiClient`](https://use-q.dev/docs/react/create-api-client.md) is that place: one schema produces `useQ`, `useM`, `queryKeys`, and the fetcher. [React Query API Design — Lessons Learned](https://tkdodo.eu/blog/react-query-api-design-lessons-learned) is the longer argument for co-locating those. ### Hierarchical query keys you never type twice [Effective React Query Keys](https://tkdodo.eu/blog/effective-react-query-keys): array keys, coarse-to-fine, plus a factory. use-q keys are always `["api", method, resolvedPath, searchParams]`. The client's `queryKeys` factory is the factory. The query function always receives TanStack's [`queryFn` context](https://tkdodo.eu/blog/leveraging-the-query-function-context) (including `signal`) — you don't thread it yourself. See [Query keys](https://use-q.dev/docs/guides/query-keys.md). ### Automatic invalidation after mutations [Automatic Query Invalidation after Mutations](https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations) is the problem `tags` / `invalidatesTags` solve. You describe which reads depend on which writes in the schema; `useM` invalidates matching queries in `onSettled`, as [Mastering Mutations](https://tkdodo.eu/blog/mastering-mutations-in-react-query) recommends — success _and_ error, so rollbacks still refetch the truth. See [Tag invalidation](https://use-q.dev/docs/guides/tag-invalidation.md). ### Transforms and render isolation via `select` Don't map in the `queryFn`. Pass `select` to [`useQ`](https://use-q.dev/docs/react/use-q.md) — [data transformations](https://tkdodo.eu/blog/react-query-data-transformations), [render optimizations](https://tkdodo.eu/blog/react-query-render-optimizations), and [selectors, supercharged](https://tkdodo.eu/blog/react-query-selectors-supercharged). ### Types inferred from the schema, not from generics [React Query and TypeScript](https://tkdodo.eu/blog/react-query-and-type-script) and [Type-safe React Query](https://tkdodo.eu/blog/type-safe-react-query): let the query function (here: the `RouteDefinition`) produce the types. You almost never pass generics to `useQ` / `useM`. ### Errors and status as Query intended them [React Query Error Handling](https://tkdodo.eu/blog/react-query-error-handling) — handle errors where you have the context: globally (`onError` on the fetcher), per mutation, or with an error boundary ([`ApiErrorBoundary`](https://use-q.dev/docs/react/api-error-boundary.md) + [`useSuspenseQ`](https://use-q.dev/docs/react/use-suspense-q.md)). [Status Checks in React Query](https://tkdodo.eu/blog/status-checks-in-react-query) still apply: prefer `isPending` / `isError` / `isSuccess` over treating `isLoading` as the only gate. ### Optimistic updates that cancel in-flight queries [`useM`](https://use-q.dev/docs/react/use-m.md)'s `optimisticUpdates` follow [Concurrent Optimistic Updates](https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query): `cancelQueries`, snapshot, `setQueryData`, rollback on error, invalidate on settle. See [Optimistic updates](https://use-q.dev/docs/guides/optimistic-updates.md). ### Seed the cache; don't copy server state into React state The cache _is_ the store for server data — [React Query as a State Manager](https://tkdodo.eu/blog/react-query-as-a-state-manager), [Thinking in React Query](https://tkdodo.eu/blog/thinking-in-react-query), [Why You Want React Query](https://tkdodo.eu/blog/why-you-want-react-query). Prefetch and hydrate with [`useQClient`](https://use-q.dev/docs/react/use-q-client.md) and [SSR & loaders](https://use-q.dev/docs/guides/ssr-and-loaders.md) ([seeding the query cache](https://tkdodo.eu/blog/seeding-the-query-cache), [placeholder vs initial data](https://tkdodo.eu/blog/placeholder-and-initial-data-in-react-query), [React Query meets React Router](https://tkdodo.eu/blog/react-query-meets-react-router)). Put a `QueryClient` in context, not query results — [React Query and React Context](https://tkdodo.eu/blog/react-query-and-react-context). ## What we don't bake in The series covers more than a client library should own. use-q leaves these to your app — and to TkDodo: - **WebSockets** — [Using WebSockets with React Query](https://tkdodo.eu/blog/using-web-sockets-with-react-query). Query is a cache; push updates into it with `useQClient().setData` / `queryClient.setQueriesData`. - **Offline persistence** — [Offline React Query](https://tkdodo.eu/blog/offline-react-query). Pass your own `QueryClient` (see [Bring your own QueryClient](https://use-q.dev/docs/guides/byo-query-client.md)). - **Forms** — [React Query and Forms](https://tkdodo.eu/blog/react-query-and-forms). `useM` submits; form state stays in the form library. - **Testing** — [Testing React Query](https://tkdodo.eu/blog/testing-react-query). One `QueryClient` per test, `queries.retry: false`. - **Whether you need Query at all** — [You Might Not Need React Query](https://tkdodo.eu/blog/you-might-not-need-react-query). use-q is for server state that benefits from a cache. Two more posts are useful background without being APIs we wrap: [Inside React Query](https://tkdodo.eu/blog/inside-react-query) (how the observer model works) and [React Query — The Bad Parts](https://tkdodo.eu/blog/react-query-the-bad-parts) (query keys and invalidation boilerplate are two of the sharp edges this library exists to blunt). [TanStack Router and Query](https://tkdodo.eu/blog/tanstack-router-and-query) is the Router-specific integration; our [SSR & loaders](https://use-q.dev/docs/guides/ssr-and-loaders.md) guide shows the shared-cache pattern without duplicating that post. ## Feature → post | In use-q | Read TkDodo | | --- | --- | | [`createApiClient`](https://use-q.dev/docs/react/create-api-client.md) | [#31 Creating Query Abstractions](https://tkdodo.eu/blog/creating-query-abstractions), [#24 The Query Options API](https://tkdodo.eu/blog/the-query-options-api) | | [Query keys](https://use-q.dev/docs/guides/query-keys.md) + `queryKeys` | [#8 Effective React Query Keys](https://tkdodo.eu/blog/effective-react-query-keys) | | [`tags` / `invalidatesTags`](https://use-q.dev/docs/guides/tag-invalidation.md) | [#25 Automatic Query Invalidation after Mutations](https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations) | | [`useQ`](https://use-q.dev/docs/react/use-q.md) `select` | [#2 Data Transformations](https://tkdodo.eu/blog/react-query-data-transformations), [#30 Selectors, Supercharged](https://tkdodo.eu/blog/react-query-selectors-supercharged) | | `placeholderData` / `initialData` | [#9 Placeholder and Initial Data](https://tkdodo.eu/blog/placeholder-and-initial-data-in-react-query) | | [`useM`](https://use-q.dev/docs/react/use-m.md) + `onSettled` invalidation | [#12 Mastering Mutations](https://tkdodo.eu/blog/mastering-mutations-in-react-query) | | [Optimistic updates](https://use-q.dev/docs/guides/optimistic-updates.md) | [#29 Concurrent Optimistic Updates](https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query) | | [`useInfiniteQ`](https://use-q.dev/docs/react/use-infinite-q.md) | [#26 How Infinite Queries work](https://tkdodo.eu/blog/how-infinite-queries-work) | | [`ApiErrorBoundary`](https://use-q.dev/docs/react/api-error-boundary.md) | [#11 Error Handling](https://tkdodo.eu/blog/react-query-error-handling) | | [SSR & loaders](https://use-q.dev/docs/guides/ssr-and-loaders.md) | [#16 React Query meets React Router](https://tkdodo.eu/blog/react-query-meets-react-router), [#17 Seeding the Query Cache](https://tkdodo.eu/blog/seeding-the-query-cache) | The series index — and every later part — lives on [Practical React Query (#1)](https://tkdodo.eu/blog/practical-react-query). There is also a [React Query FAQs](https://tkdodo.eu/blog/react-query-fa-qs) post for the questions that come up after the defaults click. --- # Installation > Install use-q and its React bindings with pnpm, npm, or yarn. `use-q` ships as two installable packages plus an optional codegen CLI: | Package | What it provides | | --- | --- | | `@use-q/api-client` | Framework-agnostic `createFetcher` + types. Zero runtime dependencies. | | `@use-q/api-client-react` | `createApiClient` and hooks (`useQ`, `useM`, …) on top of TanStack Query v5. | | `@use-q/api-client-codegen` | OpenAPI → `RouteDefinition` map generator. Ships the `use-q-codegen` CLI bin. | ## Requirements - **Node.js 18 or newer.** `use-q` relies on global `fetch` and `AbortController`. - For the React layer: **React 18+** and **`@tanstack/react-query` 5.x**. ## Install **pnpm** ```bash # Core only — server scripts, edge workers, RSC, loaders pnpm add @use-q/api-client # Plus the React bindings pnpm add @use-q/api-client @use-q/api-client-react @tanstack/react-query react react-dom ``` **npm** ```bash npm install @use-q/api-client npm install @use-q/api-client @use-q/api-client-react @tanstack/react-query react react-dom ``` **yarn** ```bash yarn add @use-q/api-client yarn add @use-q/api-client @use-q/api-client-react @tanstack/react-query react react-dom ``` ## Peer dependencies `@use-q/api-client-react` declares peer dependencies that you must install in your app: ```json { "peerDependencies": { "@tanstack/react-query": "^5.0.0", "react": "^18.0.0 || ^19.0.0" } } ``` > **Tip:** > If you're already using TanStack Query elsewhere in your app, you can share a > single `QueryClient` across both — see > [Bring your own QueryClient](https://use-q.dev/docs/guides/byo-query-client.md). ## TypeScript `use-q` is written in strict TypeScript with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`. For the best inference, enable strict mode in your `tsconfig.json` — [React Query and TypeScript](https://tkdodo.eu/blog/react-query-and-type-script) and [Type-safe React Query](https://tkdodo.eu/blog/type-safe-react-query) are why we never ask you to pass generics to `useQ` / `useM`. ```json { "compilerOptions": { "strict": true, "moduleResolution": "bundler", "target": "ES2020" } } ``` ## Verifying the install After installing, you should be able to import from each package without type errors: ```ts import { createFetcher, isApiError } from "@use-q/api-client"; import { createApiClient, ApiErrorBoundary } from "@use-q/api-client-react"; ``` ## Running the codegen CLI The `use-q-codegen` bin is shipped by the `@use-q/api-client-codegen` package. Add it as a dev dependency, then run it: ```bash pnpm add -D @use-q/api-client-codegen pnpm exec use-q-codegen --input ./openapi.yaml --output ./src/api/schema.ts ``` Or with npm: ```bash npm install -D @use-q/api-client-codegen npx use-q-codegen --input ./openapi.yaml --output ./src/api/schema.ts ``` See [Codegen](https://use-q.dev/docs/core/codegen.md) for a full walkthrough. ## Next Once everything's installed, head to the [Quick Start](https://use-q.dev/docs/getting-started/quick-start.md) for a five-minute end-to-end example. --- # Quick start > Build a type-safe list + create flow with use-q in five minutes. This guide walks through a complete, runnable example: define a small schema, create a client, mount the provider, and use `useQ` / `useM` from a component. We'll use a fictional API with three resources — `facilities`, `posts`, and `comments` — and a single `facilityId` path parameter. The same example is reused across the docs. ### Install the packages ```bash pnpm add @use-q/api-client @use-q/api-client-react @tanstack/react-query react react-dom ``` See [Installation](https://use-q.dev/docs/getting-started/installation.md) for npm/yarn equivalents and peer deps. ### Define a schema Create `src/api/schema.ts`. Each route is typed with `satisfies RouteDefinition` — the four type arguments carry path params, search params, request body, and response shape: ```ts import type { RouteDefinition } from "@use-q/api-client"; export interface Post { id: string; facilityId: string; title: string; body: string; createdAt: string; } export interface CreatePostInput { title: string; body: string; } export const schema = { listPosts: { method: "GET", path: "/facilities/{facilityId}/posts", tags: ["posts"], } satisfies RouteDefinition<{ facilityId: string }, { search?: string }, never, Post[]>, getPost: { method: "GET", path: "/facilities/{facilityId}/posts/{postId}", tags: ({ params }) => [{ type: "post", id: params.postId }], } satisfies RouteDefinition<{ facilityId: string; postId: string }, never, never, Post>, createPost: { method: "POST", path: "/facilities/{facilityId}/posts", invalidatesTags: ["posts"], } satisfies RouteDefinition<{ facilityId: string }, never, CreatePostInput, Post>, } as const; ``` ### Create the client In `src/api/client.ts`, instantiate the client once and re-export the hooks you'll use across the app: ```ts import { createApiClient } from "@use-q/api-client-react"; import { schema } from "./schema"; export const api = createApiClient(schema, { baseUrl: import.meta.env.VITE_API_BASE_URL ?? "https://api.example.com", headers: () => ({ Authorization: `Bearer ${localStorage.getItem("token") ?? ""}`, }), }); export const { useQ, useM, useInfiniteQ, useQClient, queryClient } = api; ``` ### Wrap your app in `QueryClientProvider` `createApiClient` returns the underlying `queryClient` — use it directly so every hook shares the same cache: ```tsx import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { QueryClientProvider } from "@tanstack/react-query"; import { api } from "./api/client"; import { App } from "./App"; createRoot(document.getElementById("root")!).render( , ); ``` ### Read with `useQ` `useQ` takes the route id, an input object (`params` + `searchParams`), and optionally any TanStack `useQuery` options: ```tsx import { useQ } from "./api/client"; export function PostList({ facilityId }: { facilityId: string }) { const { data, isLoading, error } = useQ("listPosts", { params: { facilityId }, searchParams: { search: "" }, }); if (isLoading) return

Loading posts…

; if (error) return

Failed to load: {error.message}

; return ( ); } ``` ### Write with `useM` `useM` takes the route id (plus optional mutation options); path params, body, and search params are all passed as variables to `mutate()`: ```tsx import { useM } from "./api/client"; export function NewPostForm({ facilityId }: { facilityId: string }) { const createPost = useM("createPost"); return (
{ e.preventDefault(); const form = new FormData(e.currentTarget); createPost.mutate({ params: { facilityId }, body: { title: String(form.get("title")), body: String(form.get("body")), }, }); }} >