useQ
useQ is the read-side hook. It’s a thin, typed wrapper around useQuery from TanStack Query — same return shape, same options, with parameters constrained to a schema route.
const { data, isLoading, error, refetch } = useQ("listPosts", { pathParams: { facilityId: "f1" }, searchParams: { search: "" },});// ^? data: Post[] | undefinedSignature
Section titled “Signature”function useQ< TRoute extends ReadableRouteKey<TSchema>, TSelect = RouteResponse<TSchema, TRoute>,>( routeName: TRoute, params: RouteParams<TSchema, TRoute> & UseQOptions<…, TSelect>,): UseQueryResult<TSelect, ApiError<TParsedError>>;The first argument is the route name. The second is a single object containing both the request params and any options.
Required params
Section titled “Required params”For a route with pathParams and/or searchParams, those keys are required (or optional if the schema marks them optional):
useQ("getPost", { pathParams: { facilityId: "f1", postId: "p1" },});
useQ("listPosts", { pathParams: { facilityId: "f1" }, searchParams: { search: "react" }, // optional});UseQOptions
Section titled “UseQOptions”Every option below sits alongside the request params on the same object:
interface UseQOptions<TData, TError, TSelect> { enabled?: boolean; select?: (data: TData) => TSelect; staleTime?: number; gcTime?: number; refetchInterval?: number | false; refetchOnWindowFocus?: boolean | "always"; refetchOnReconnect?: boolean | "always"; retry?: boolean | number | ((failureCount: number, error: TError) => boolean); placeholderData?: TData | ((prev: TData | undefined) => TData); initialData?: TData | (() => TData); meta?: Record<string, unknown>;}enabled — conditional fetching
Section titled “enabled — conditional fetching”Guard a query on data from another query, route params, or feature flags:
function PostDetail({ facilityId, postId }: { facilityId: string; postId?: string }) { const post = useQ("getPost", { pathParams: { facilityId, postId: postId! }, enabled: Boolean(postId), }); if (!postId) return <p>Pick a post</p>; return <h1>{post.data?.title}</h1>;}select — project the response
Section titled “select — project the response”Avoid re-rendering components that only need a slice of the response. select is memoized by TanStack Query, so the result is referentially stable as long as the selector returns the same value.
const titles = useQ("listPosts", { pathParams: { facilityId: "f1" }, select: (posts) => posts.map((p) => p.title),});// titles.data is now string[]A common pattern: derive a normalized lookup map without forcing the rest of the app to rebuild it.
const postsById = useQ("listPosts", { pathParams: { facilityId: "f1" }, select: (posts) => Object.fromEntries(posts.map((p) => [p.id, p])),});staleTime — when to consider data fresh
Section titled “staleTime — when to consider data fresh”Defaults to 0 (always stale → refetch on mount / focus). Increase it for slow-changing data:
const settings = useQ("getSettings", { pathParams: { facilityId: "f1" }, staleTime: 5 * 60_000,});refetchInterval — polling
Section titled “refetchInterval — polling”const jobStatus = useQ("getJobStatus", { pathParams: { jobId }, refetchInterval: (data) => (data?.status === "running" ? 1000 : false),});Pass a function to stop polling once a terminal state is reached.
refetchOnWindowFocus / refetchOnReconnect
Section titled “refetchOnWindowFocus / refetchOnReconnect”Both default to true. Disable for queries where stale data is acceptable but a flash of refetch would be jarring:
useQ("getCurrentUser", { staleTime: 60_000, refetchOnWindowFocus: false,});Standard TanStack Query semantics — pass a number or a predicate. The error here is fully typed:
import { isApiError } from "@use-q/api-client-react";
useQ("getPost", { pathParams: { facilityId, postId }, retry: (count, error) => { if (isApiError(error) && error.status === 404) return false; return count < 3; },});placeholderData
Section titled “placeholderData”Render something while the real data loads:
useQ("listPosts", { pathParams: { facilityId: "f1" }, placeholderData: [] as Post[],});Or — for “keep previous data” while paginating — use keepPreviousData from TanStack Query:
import { keepPreviousData } from "@tanstack/react-query";
useQ("listPosts", { pathParams: { facilityId }, searchParams: { page }, placeholderData: keepPreviousData,});initialData
Section titled “initialData”Hydrate the cache from a server-fetched payload (RSC / loader):
useQ("listPosts", { pathParams: { facilityId: "f1" }, initialData: () => loaderData.posts,});See SSR & loaders for the full pattern.
Returned shape
Section titled “Returned shape”useQ returns TanStack Query’s UseQueryResult — { data, error, isLoading, isFetching, isError, isSuccess, refetch, … }. The only difference: error is ApiError<TParsedError> instead of unknown.
Query keys
Section titled “Query keys”Every useQ call uses the key:
["api", method, resolvedPath, searchParams];// e.g. ["api", "GET", "/facilities/f1/posts", { search: "" }]This shape makes prefix invalidation natural. See Query keys.
Cancellation
Section titled “Cancellation”useQ already passes an AbortSignal to the fetcher when the component unmounts or the key changes — your fetchFn receives it as init.signal. Don’t pass signal manually; let TanStack Query own it.