createApiClient
createApiClient is the entry point for the React layer. Given a Schema, it returns a bundle of hooks and helpers that share one QueryClient, one Fetcher, and one TagRegistry.
import { createApiClient } from "@use-q/api-client-react";import type { Schema } from "@use-q/api-client";import { schema } from "./schema";
export const api = createApiClient<typeof schema>(schema, { baseUrl: "https://api.example.com",});Options
Section titled “Options”createApiClient accepts everything createFetcher does, plus a few React-specific knobs:
interface CreateApiClientOptions<TParsedError = unknown> { baseUrl: string; fetchFn?: typeof fetch; defaultHeaders?: | Record<string, string> | (() => Promise<Record<string, string>> | Record<string, string>); parseError?: ( res: Response, body: unknown, ) => TParsedError | Promise<TParsedError>; onError?: ( err: ApiError<TParsedError>, ctx: { routeName: string; method: string; url: string; status: number }, ) => void; queryClient?: QueryClient;}See Core options for the fetcher-side fields. The React-only queryClient is documented in Bring your own QueryClient.
Returned shape
Section titled “Returned shape”interface ApiClient<TSchema extends Schema, TParsedError = unknown> { // Hooks useQ: UseQ<TSchema, TParsedError>; useM: UseM<TSchema, TParsedError>; useInfiniteQ: UseInfiniteQ<TSchema, TParsedError>; useSuspenseQ: UseSuspenseQ<TSchema, TParsedError>; useQClient: () => QClient<TSchema>;
// Plumbing fetcher: FetcherInstance<TSchema, TParsedError>; queryClient: QueryClient; queryKeys: QueryKeyFactory<TSchema>; isApiError: typeof isApiError;
// Internal — exposed for advanced cases _tagRegistry: TagRegistry;}The hook references are stable across renders — they’re created once in the closure, so it’s safe to destructure them at module scope:
export const { useQ, useM, useInfiniteQ, useQClient } = api;Recommended pattern: src/api/client.ts
Section titled “Recommended pattern: src/api/client.ts”Create the client in one place and re-export hooks so consumers never see the bare api.useQ(...) form:
import { createApiClient } from "@use-q/api-client-react";import { schema } from "./schema";
export const api = createApiClient<typeof schema>(schema, { baseUrl: import.meta.env.VITE_API_BASE_URL, defaultHeaders: () => ({ Authorization: `Bearer ${tokenStore.get() ?? ""}`, }), onError: (err) => { if (err.status === 401) tokenStore.clear(); },});
export const { useQ, useM, useInfiniteQ, useSuspenseQ, useQClient, queryClient,} = api;Components then read hooks with zero ceremony:
import { useQ } from "@/api/client";
function PostList() { const { data } = useQ("listPosts", { pathParams: { facilityId: "f1" } }); return <ul>{data?.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;}Wrapping your app
Section titled “Wrapping your app”createApiClient constructs its own QueryClient, which you pass to QueryClientProvider:
import { QueryClientProvider } from "@tanstack/react-query";import { api } from "@/api/client";
export function Root({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={api.queryClient}> {children} </QueryClientProvider> );}If you’d rather share a QueryClient with something else (e.g. another createApiClient instance, or a non-use-q query), pass it via options.queryClient:
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, refetchOnWindowFocus: false }, },});
const api = createApiClient<typeof schema>(schema, { baseUrl: "https://api.example.com", queryClient,});See BYO QueryClient for the full discussion.
Typing with a custom parsed-error shape
Section titled “Typing with a custom parsed-error shape”Pass the parsed-error type as the second generic:
interface ApiProblem { type: string; title: string; detail: string;}
export const api = createApiClient<typeof schema, ApiProblem>(schema, { baseUrl: "https://api.example.com", parseError: async (res) => { const json = (await res.json().catch(() => null)) as ApiProblem | null; return json ?? { type: "about:blank", title: res.statusText, detail: "" }; },});Now every hook’s error.parsed is typed as ApiProblem.
Multiple clients
Section titled “Multiple clients”You can call createApiClient more than once per app — useful for unrelated APIs (e.g. internal API + public API, or distinct microservices):
export const internal = createApiClient<typeof internalSchema>(internalSchema, { baseUrl: "https://internal.example.com", queryClient,});
export const public_ = createApiClient<typeof publicSchema>(publicSchema, { baseUrl: "https://public.example.com", queryClient,});useQ— queryinguseM— mutatinguseQClient— manual cache control<ApiErrorBoundary>— suspense error handling