use-q

Command Palette

Search for a command to run...

CreateApiClientOptions

Every option accepted by createApiClient — extends CreateFetcherOptions with queryClient.

function createApiClient<TSchema extends Schema>(
  schema: TSchema,
  options: CreateApiClientOptions,
): ApiClient<TSchema>;
 
interface CreateApiClientOptions extends CreateFetcherOptions {
  queryClient?: QueryClient;
}

CreateApiClientOptions is CreateFetcherOptions plus one extra field. Everything from Fetcher options applies; the React-specific knob is queryClient.

Note that the schema is a runtime argument, not just a type parameter — the hooks read tags, invalidatesTags, and pagination off the route definitions at runtime to drive invalidation and infinite queries.

Fetcher-inherited fields

These behave exactly like in createFetcher. Refer to that page for the full discussion:

FieldTypeRequired
baseUrlstringyes
headersHeadersInit or sync/async function returning oneno
fetchtypeof fetchno
parseError({ response, data }) => unknownno
onError(error) => voidno

React-only fields

queryClient

TypeDefault
QueryClient (from @tanstack/react-query)a freshly constructed QueryClient

If you provide a QueryClient, createApiClient uses it as-is. If not, it constructs one with TanStack Query's defaults.

Use cases:

  • Share defaults across multiple createApiClient instances.
  • Configure defaultOptions (staleTime, gcTime, retry).
  • Hook into persistence, devtools, or other TanStack Query primitives.
  • Embed use-q into an existing TanStack Query setup.
import { QueryClient } from "@tanstack/react-query";
import { createApiClient } from "@use-q/api-client-react";
 
const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 30_000, retry: 1, refetchOnWindowFocus: false },
    mutations: { retry: 0 },
  },
});
 
export const api = createApiClient(schema, {
  baseUrl: "https://api.example.com",
  queryClient,
});

See BYO QueryClient for full patterns (persistence, devtools, multiple clients).

Returned shape

const api = createApiClient(schema, options);
 
api.useQ;          // (routeId, input?, options?) — read hook
api.useM;          // (routeId, options?) — write hook
api.useInfiniteQ;  // (routeId, input?, options?) — paginated read hook
api.useSuspenseQ;  // (routeId, input?, options?) — Suspense read hook
api.useQClient;    // () — typed cache-control hook
 
api.fetcher;       // FetcherInstance
api.queryClient;   // QueryClient
api.queryKeys;     // QueryKeysFactory<TSchema>
api.isApiError;    // type guard, re-exported for convenience
api.schema;        // the schema you passed in
api._tagRegistry;  // internal TagRegistry (semi-private)
FieldPurpose
useQRead hook (TanStack Query useQuery semantics). Called as useQ(routeId, input?, options?) where input = { params?, searchParams? }.
useMWrite hook with optimistic updates + tag invalidation. Called as useM(routeId, options?); variables { params?, body?, searchParams? } go to mutate.
useInfiniteQPaginated reads. Requires the route to have a pagination block — throws otherwise.
useSuspenseQSuspense-friendly read, same signature as useQ.
useQClientReturns { invalidateTag, invalidate, invalidateAll, setData, updateData, prefetch }.
fetcherThe framework-agnostic fetcher backing all hooks. Use in loaders/RSC.
queryClientThe QueryClient (yours, or constructed). Pass to <QueryClientProvider>.
queryKeysPer-route key factory: api.queryKeys.routeId(input?) returns the canonical ["api", METHOD, resolvedPath, sortedSearchParams] key, where input = { params?, searchParams? }.
isApiErrorType guard, re-exported for convenience.
schemaThe runtime schema, exposed for introspection.
_tagRegistryInternal TagRegistry. Exposed for testing and advanced patterns — treat as semi-private.

Type parameters

createApiClient<TSchema extends Schema>(schema, options);

TSchema is inferred from the schema argument, so you rarely write it explicitly — every hook, queryKeys entry, and useQClient method is typed from it.

Error typing is handled at catch sites rather than as a client-level generic: shape the payload with parseError and narrow with isApiError<T>:

interface ApiProblem {
  type: string;
  title: string;
  detail: string;
}
 
export const api = createApiClient(schema, {
  baseUrl: "https://api.example.com",
  parseError: ({ response, data }) => {
    const json = data as Partial<ApiProblem> | null;
    return {
      type: json?.type ?? "about:blank",
      title: json?.title ?? response.statusText,
      detail: json?.detail ?? "",
    } satisfies ApiProblem;
  },
});
 
// Anywhere an error surfaces:
if (api.isApiError<ApiProblem>(error)) {
  console.error(error.data.title);
}

See also