Skip to content

React QueryuseQuery

useQuery

Fetch data with automatic caching, refetching, and full type inference from your API definitions.

3 min readproxy mode

Signature

Access useQuery through the query caller's dot-notation API. Types are fully inferred from your endpoint definitions:

ts
const result = q.users.useQuery("getAll", options)

// q.<group>.useQuery(method, options?)
// group   — endpoint group name (e.g. "users"), or a group inside one: q.billing.invoices
// method  — method name within the group (e.g. "getAll")
// options — { variables?, requestOptions?, ...reactQueryOptions }

Basic query

The simplest usage — fetch data on mount. The data type is automatically inferred from the method's response type:

components/user-list.tsx
"use client"

import { q } from "@/lib/query"

export function UserList() {
  const { data, isLoading, error } = q.users.useQuery("getAll")
  //      ^? UserList — auto-inferred from .response<UserList>()

  if (isLoading) return <p>Loading...</p>
  if (error) return <p>Error: {error.message}</p>

  return (
    <ul>
      {data?.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Query with variables

When a method defines variables (e.g. .variables<{ id: number }>()), the variables option becomes required. TypeScript will error if you omit it:

components/user-profile.tsx
"use client"

import { q } from "@/lib/query"

export function UserProfile({ userId }: { userId: number }) {
  // .variables<{ id: number }>() → variables is required
  const { data: user, isLoading } = q.users.useQuery("getById", {
    variables: { id: userId },
    //          ^? { id: number } — auto-inferred from the chain
  })

  if (isLoading) return <p>Loading...</p>

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  )
}

Variables type safety

If a method never calls .variables(), the variables option is optional. If it declares a concrete variables type, the options object with variables is required.

Conditional queries

Use the enabled option to conditionally execute a query. The query won't fire until the condition is true:

components/user-profile.tsx
"use client"

import { q } from "@/lib/query"

export function UserProfile({ userId }: { userId: number | null }) {
  const { data, isLoading } = q.users.useQuery("getById", {
    variables: { id: userId! },
    // Only fetch when userId is available
    enabled: userId !== null,
  })

  if (!userId) return <p>Select a user</p>
  if (isLoading) return <p>Loading...</p>

  return <h1>{data?.name}</h1>
}

React Query options

All standard React Query options are supported alongside Courier-specific options. Configure caching, refetching, and data transformation:

ts
const { data } = q.users.useQuery("getAll", {
  variables: { page: 1, limit: 20 },

  // Standard React Query options — all supported
  staleTime: 5 * 60 * 1000,       // Consider data fresh for 5 minutes
  refetchInterval: 30 * 1000,     // Refetch every 30 seconds
  refetchOnWindowFocus: false,     // Don't refetch on tab focus
  placeholderData: previousData,   // Keep previous data while refetching
  select: (data) => data.data,     // Transform response before caching

  // Courier-specific options
  requestOptions: {
    timeout: 5000,   // The options .with() takes, per call
  },
})

Auto-generated query keys

Courier automatically generates hierarchical query keys from the source, group, method, and variables. This enables cache invalidation at different levels of specificity:

ts
// Auto-generated query key: ["api", "users", "getAll"]
q.users.useQuery("getAll")

// With variables: ["api", "users", "getById", { id: 5 }]
q.users.useQuery("getById", { variables: { id: 5 } })

// Invalidate a specific method
queryClient.invalidateQueries({ queryKey: ["api", "users", "getAll"] })

// Invalidate all queries for a group
queryClient.invalidateQueries({ queryKey: ["api", "users"] })

// Invalidate all queries for the source
queryClient.invalidateQueries({ queryKey: ["api"] })

Group-level invalidation

Unlike flat key formats, hierarchical keys let you invalidate all queries for a group at once. ["api", "users"] matches getAll, getById, create — everything in the users group.

Error handling

The error property is typed as Error, as TanStack Query types it, because not every failure is an HTTP one. It is an HTTPError for an error answer or a call that got no answer, a ValidationError when variables or the answer fail their schema, a ConfigurationError when the client is set up wrongly, the reason a call was cancelled with through your own signal, or whatever a select threw. The same holds for useSuspenseQuery, useInfiniteQuery, useMutation and useComputedQuery. Use isHTTPError to check if it's an HTTP error with status and response details, and isValidationError or isConfigurationError for the others:

components/user-list.tsx
"use client"

import { isHTTPError } from "@noverax/courier"
import { q } from "@/lib/query"

export function UserList() {
  const { data, error, isError } = q.users.useQuery("getAll")

  if (isError) {
    // Check if it's an HTTP error with status/response
    if (isHTTPError(error)) {
      if (error.status === 404) return <p>Not found</p>
      if (error.status === 403) return <p>Access denied</p>
    }
    return <p>Something went wrong: {error.message}</p>
  }

  return <ul>{data?.data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}

Next steps

  • useMutation — create, update, and delete data with typed mutations
  • Advanced Patterns — prefetching, hydration, optimistic updates, and cache strategies
  • Error Handling — deep dive into HTTPError and error recovery