Import
import { createProxyQueryCaller } from "@noverax/courier-react"Peer dependency
@noverax/courier-react requires @tanstack/react-query as a peer dependency. It re-exports QueryClient, QueryClientProvider, useQueryClient, HydrationBoundary, and dehydrate for convenience.
Signature
Accepts a source name and ProxyCallerDefaults (same as createProxyCaller). The defaults are optional for an API of plain requests; an API with streams or channels requires a boundary (or the source) among them, as createProxyCaller does. Returns a Proxy-based client with dot-notation access to hooks for each group.
import type { ProxyCallerArguments } from "@noverax/courier"
import type { DefinedApi } from "@noverax/courier/define"
import type { QueryCallerClient } from "@noverax/courier-react"
declare function createProxyQueryCaller<TApi extends DefinedApi>(
source: string,
...defaults: ProxyCallerArguments<TApi>
): QueryCallerClient<TApi>Return Type
The returned object provides dot-notation access to every group defined in your API. Each group exposes:
type QueryGroupClient<G> = {
useQuery(method, options?): UseQueryResult<R, Error>
useSuspenseQuery(method, options?): UseSuspenseQueryResult<R, Error>
useInfiniteQuery(method, options): UseInfiniteQueryResult<R, Error>
useMutation(method, options?): UseMutationResult<R, Error, V>
prefetchQuery(queryClient, method, options?): Promise<void>
}A hook's error is typed Error, as TanStack Query types it, because not every failure is an HTTP one: narrow it with isHTTPError, isValidationError or isConfigurationError from @noverax/courier. See useQuery — Error handling.
Setup
import { createProxyQueryCaller } from "@noverax/courier-react"
import type { api } from "@/api"
import { boundary } from "@/api/boundary"
export const q = createProxyQueryCaller<typeof api>("api", {
boundary,
proxyPath: { http: "/api/request" }, // optional, defaults to /api/request
timeout: 10000,
credentials: "include",
})The defaults argument is a ProxyCallerDefaults (same shape as createProxyCaller). Notable fields:
proxyPath— per-transport endpoint paths.httpandssehave built-in defaults;wsis mandatory foruseChanneland has no default. Caller-level fields are merged per-transport oversource.options.proxyPath.ws— options of the socketuseChannelopens: the token, the reconnect backoff, the heartbeat and the queue limits — see Connection options.boundary— the result ofdefineBoundary: the endpoints the browser may reach and the app's proxy paths. Pass it rather than the API object — the hooks name their own transport, so nothing is read off the API, and a value import of it would put its definition in the bundle.
useChannel requires proxyPath.ws
On runtimes that cannot host WebSockets in-process (Next.js App Router, Cloudflare Workers, Vercel Edge, AWS Lambda) you must point proxyPath.ws at a standalone @noverax/courier-server. A useChannel without proxyPath.ws configured reports ConfigurationError("WS_PATH_NOT_CONFIGURED") through its error and onError when it first opens.
useQuery
Wraps @tanstack/react-query's useQuery. All standard options are supported.
"use client"
import { q } from "@/lib/query"
export function UserList() {
const { data, isLoading, error } = q.users.useQuery("getAll", {
staleTime: 60_000,
refetchOnWindowFocus: false,
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}Variables
When an endpoint expects variables, the variables option becomes required — enforced by TypeScript.
const { data } = q.users.useQuery("getById", {
variables: { id: "123" },
enabled: !!id,
})useMutation
Variables are passed to the mutate function, not to the hook.
"use client"
import { q } from "@/lib/query"
import { useQueryClient } from "@noverax/courier-react"
export function CreateUserForm() {
const queryClient = useQueryClient()
const { mutate, isPending } = q.users.useMutation("create", {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["api", "users", "getAll"] })
},
})
return (
<form onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
mutate({ name: formData.get("name") as string })
}}>
<input name="name" required />
<button disabled={isPending}>
{isPending ? "Creating..." : "Create"}
</button>
</form>
)
}Query Keys
Query keys are auto-generated with a hierarchical format: [source, group, method, variables?]:
// Without variables: [source, group, method]
// With variables: [source, group, method, variables]
q.users.useQuery("getAll") // → ["api", "users", "getAll"]
q.users.useQuery("getById", {
variables: { id: "123" }
}) // → ["api", "users", "getById", { id: "123" }]
// An infinite query adds its first page's variables and an "infinite" marker,
// so each filter gets its own list and no plain query shares its entry
q.posts.useInfiniteQuery("list", {
variables: (cursor) => ({ cursor, tag: "news" }),
initialPageParam: 0,
getNextPageParam: (page) => page.nextCursor,
}) // → ["api", "posts", "list", { cursor: 0, tag: "news" }, "infinite"]
// Cache invalidation
queryClient.invalidateQueries({ queryKey: ["api", "users", "getAll"] })
queryClient.invalidateQueries({ queryKey: ["api", "users"] }) // entire group
queryClient.invalidateQueries({ queryKey: ["api"] }) // entire sourcePer-Request Options
Use requestOptions to pass CallOptions alongside React Query options:
const { data } = q.users.useQuery("getAll", {
requestOptions: {
timeout: 5000,
method: "POST", // keep this call's variables out of the address
},
staleTime: 30_000,
})
const { mutate } = q.files.useMutation("upload", {
requestOptions: {
onUploadProgress: (progress) => {
setProgress(progress.percent)
},
},
})prefetchQuery
Prefetch data in Server Components for instant hydration on the client. On the server there is no page to resolve a relative proxyPath against, so a proxy caller cannot run there unless proxyPath.http is an absolute URL. Prefetch with a direct caller built from the definition instead — the server has it — and name the definition after the proxy's source, so both build the same keys:
import { createQueryCaller } from "@noverax/courier-react"
import { api } from "@/api" // defineApi({ name: "api", ... })
export const qServer = createQueryCaller(api)import { dehydrate, HydrationBoundary, QueryClient } from "@noverax/courier-react"
import { qServer } from "@/lib/query.server"
export default async function UsersPage() {
const queryClient = new QueryClient()
await qServer.users.prefetchQuery(queryClient, "getAll")
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<UserList />
</HydrationBoundary>
)
}Related
- React Query Setup — full setup guide with QueryClientProvider
- useQuery Guide — in-depth usage patterns
- useMutation Guide — mutations with optimistic updates
- createProxyCaller — the underlying proxy caller