Skip to content

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[] | undefined
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.

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
});

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>;
}

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>;
}

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])),
});

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,
});
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.

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;
},
});

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,
});

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.

useQ returns TanStack Query’s UseQueryResult{ data, error, isLoading, isFetching, isError, isSuccess, refetch, … }. The only difference: error is ApiError<TParsedError> instead of unknown.

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.

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.