useQ
Fetch a route as a typed query. select, enabled, staleTime, refetchInterval, and more.
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. Types come from the RouteDefinition, not from useQuery generics (React Query and TypeScript).
const { data, isLoading, error, refetch } = useQ("listPosts", {
params: { facilityId: "f1" },
searchParams: { search: "" },
});
// ^? data: Post[] | undefinedSignature
function useQ<RouteId extends keyof TSchema & string>(
routeId: RouteId,
input?: RouteInput<TSchema[RouteId]>,
options?: UseQOptions<RouteResponse<TSchema[RouteId]>>,
): UseQueryResult<RouteResponse<TSchema[RouteId]>>;Three positional arguments:
routeId— a key of your schema.input— the request input:{ params?, searchParams? }.paramsfills the{placeholder}segments of the route'spath;searchParamsbecomes the query string.options— any TanStackuseQueryoptions exceptqueryKeyandqueryFn(the hook owns those).
Input
For a route with path params and/or search params, pass them via input:
useQ("getPost", {
params: { facilityId: "f1", postId: "p1" },
});
useQ("listPosts", {
params: { facilityId: "f1" },
searchParams: { search: "react" }, // optional
});A route with no input can omit the argument entirely: useQ("listFacilities").
UseQOptions
Options are the third argument — a separate object from the input:
type UseQOptions<TData> = Omit<
UseQueryOptions<TData, DefaultError, TData, QueryKey>,
"queryKey" | "queryFn"
>;That means everything useQuery supports is available: enabled, select, staleTime, gcTime, refetchInterval, refetchOnWindowFocus, refetchOnReconnect, retry, placeholderData, initialData, meta, and so on.
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",
{ params: { facilityId, postId: postId! } },
{ enabled: Boolean(postId) },
);
if (!postId) return <p>Pick a post</p>;
return <h1>{post.data?.title}</h1>;
}select — project the response
Avoid re-rendering components that only need a slice of the response. Transform with select, not inside the query function — that's TkDodo's data-transformation and render-optimization advice, taken further in selectors, supercharged. 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",
{ params: { 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",
{ params: { facilityId: "f1" } },
{ select: (posts) => Object.fromEntries(posts.map((p) => [p.id, p])) },
);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",
{ params: { facilityId: "f1" } },
{ staleTime: 5 * 60_000 },
);refetchInterval — polling
const jobStatus = useQ(
"getJobStatus",
{ params: { jobId } },
{
refetchInterval: (query) =>
query.state.data?.status === "running" ? 1000 : false,
},
);Pass a function to stop polling once a terminal state is reached.
refetchOnWindowFocus / refetchOnReconnect
Both default to true. Disable for queries where stale data is acceptable but a flash of refetch would be jarring:
useQ("getCurrentUser", undefined, {
staleTime: 60_000,
refetchOnWindowFocus: false,
});retry
Standard TanStack Query semantics — pass a number or a predicate. Narrow the error with isApiError:
import { isApiError } from "@use-q/api-client-react";
useQ(
"getPost",
{ params: { facilityId, postId } },
{
retry: (count, error) => {
if (isApiError(error) && error.status === 404) return false;
return count < 3;
},
},
);placeholderData
Render something while the real data loads. This does not put the value in the cache — unlike initialData. See Placeholder and Initial Data for when to use which.
useQ(
"listPosts",
{ params: { 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",
{ params: { facilityId }, searchParams: { page } },
{ placeholderData: keepPreviousData },
);initialData
Hydrate the cache from a server-fetched payload (RSC / loader):
useQ(
"listPosts",
{ params: { facilityId: "f1" } },
{ initialData: () => loaderData.posts },
);See SSR & loaders for the full pattern.
Returned shape
useQ returns TanStack Query's UseQueryResult — { data, error, isPending, isLoading, isFetching, isError, isSuccess, refetch, … }. Prefer isPending / isError / isSuccess over treating isLoading as the only gate (status checks). Failures thrown by the fetcher are ApiError instances; use isApiError(error) to narrow before reading status, data, etc. See Error handling.
Tag registration
After data arrives, useQ resolves the route's tags (static array or ({ response, params }) => Tag[] function) and registers them with the client's TagRegistry — once per query key + response identity. On unmount, the registration is removed. This is what lets mutations and useQClient().invalidateTag find the query later. See Tag invalidation.
Query keys
Every useQ call uses the key:
["api", method, resolvedPath, sortedSearchParams];
// e.g. ["api", "GET", "/facilities/f1/posts", { search: "" }]This shape makes prefix invalidation natural. See Query keys.
Cancellation
useQ already passes an AbortSignal to the fetcher when the component unmounts or the key changes — that's the query function context (QueryFunctionContext.signal). A custom fetch implementation receives it via RequestInit.signal. Don't pass signal manually; let TanStack Query own it.
Want to render inside <Suspense>? Use useSuspenseQ instead — same signature, suspense semantics.