Eden TanStack Query
Guides

Queries

Fetch data with type-safe query options

Queries

Any route using GET, HEAD, or OPTIONS is treated as a query procedure. Use queryOptions() to create options for TanStack Query's useQuery.

Basic Query

Using the server from the Getting Started guide:

import { useQuery } from '@tanstack/react-query'
import { useEden } from './lib/eden'

function UserList() {
  const eden = useEden()

  const { data, isLoading, error } = useQuery(
    eden.users.get.queryOptions()
  )

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Request failed with status {error.status}</div>

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

Query with Input

Pass query parameters as the first argument:

// GET /users?role=admin&status=active
const { data } = useQuery(
  eden.users.get.queryOptions({ role: 'admin', status: 'active' })
)

The input argument is optional only when every query field is optional. A route with required query fields must receive the complete input or skipToken.

For per-request headers, use a wrapped input:

eden.users.get.queryOptions({
  query: { role: 'admin' },
  headers: { authorization: 'Bearer token' },
})

Wrapped input requires an explicit headers property. Without headers, pass { role: 'admin' } directly or { query: { role: 'admin' }, headers: undefined }. A lone { query: value } is treated as a query parameter named query, not a request wrapper. These rules also apply to infiniteQueryOptions().

If your schema has a field named query, use an outer wrapper for HTTP headers:

eden.search.get.queryOptions({
  query: { query: 'hello' },
  headers: { authorization: 'Bearer token' },
})

The flattened form { query: 'hello', headers: { authorization: 'Bearer token' } } has the same shape as a wrapper around a string query, so it is rejected for this schema. Direct { query: 'hello', headers: 'query-value' } remains valid when both fields are scalar query parameters. If the schema's headers field can be undefined and it also declares query, use a wrapper for all inputs, including inputs that omit headers.

For a direct { query: value }, omit the transport headers property entirely. Without TypeScript's exactOptionalPropertyTypes, an optional never property still accepts undefined, so types cannot reject every explicit headers: undefined. At runtime, an object containing only query and headers is a wrapper when headers is undefined or a non-null, non-array object. This also applies to untyped inputs and partial inputs passed to key and filter helpers.

If an existing wrapper variable declares headers?, pass { ...input, headers: input.headers } to make the property explicit.

If your query schema declares an object-valued field named headers, wrap the query. Direct input would send that field as HTTP headers instead of a query parameter. Migrate existing calls by adding query and headers: undefined:

eden.items.get.queryOptions({
  query: { headers: { mode: 'compact' } },
  headers: undefined,
})

This restriction also covers optional fields and fields that allow both object and scalar values. Array and other object types require a wrapper too, even when the runtime would accept them directly.

For a query union, members without colliding fields still accept direct input. If any member has a query field or an object-valued headers field, use a wrapper for HTTP headers on all members. This prevents a variable with ambiguous headers from matching a member that does not declare that field. Schemas without a collision continue to accept flattened HTTP headers, such as { role: 'admin', headers: { authorization: 'Bearer token' } }. The same input rules apply to infinite queries.

Pass additional TanStack Query options as the second argument:

const { data } = useQuery(
  eden.users.get.queryOptions(
    { role: 'admin' },
    { staleTime: 5 * 60 * 1000 }
  )
)

Path Parameters

For routes with dynamic segments, call the path segment as a function. See the Path Parameters guide for details.

// GET /users/:id
const { data } = useQuery(
  eden.users({ id: userId }).get.queryOptions()
)

useSuspenseQuery

Works with useSuspenseQuery for React Suspense support:

import { useSuspenseQuery } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: string }) {
  const eden = useEden()

  // data is guaranteed to be defined
  const { data } = useSuspenseQuery(
    eden.users({ id: userId }).get.queryOptions()
  )

  return <div>{data.name}</div>
}

Query Key

Use queryKey() for cache operations:

const queryClient = useQueryClient()
const eden = useEden()

// Invalidate all users queries
queryClient.invalidateQueries({
  queryKey: eden.users.get.queryKey()
})

// Invalidate with specific input
queryClient.invalidateQueries({
  queryKey: eden.users.get.queryKey({ role: 'admin' })
})

See the Query Keys guide for full details on key structure and cache operations.

Query Filter

Use queryFilter() for invalidation with additional filter options:

queryClient.invalidateQueries(
  eden.users.get.queryFilter()
)

// With input filtering
queryClient.invalidateQueries(
  eden.users.get.queryFilter({ role: 'admin' })
)

// With additional filter options
queryClient.invalidateQueries(
  eden.users.get.queryFilter(undefined, { exact: true, stale: true })
)

Conditional Queries with skipToken

Use skipToken to conditionally disable queries:

import { useQuery, skipToken } from '@tanstack/react-query'

function UserProfile({ userId }: { userId: string | null }) {
  const eden = useEden()

  const { data } = useQuery(
    eden.users({ id: userId ?? '' }).get.queryOptions(
      userId ? undefined : skipToken
    )
  )

  if (!userId) return <div>Select a user</div>
  return <div>{data?.name}</div>
}

Abort on Unmount

Enable request cancellation when the component unmounts:

const { data } = useQuery(
  eden.users.get.queryOptions(
    undefined,
    {
      eden: {
        abortOnUnmount: true
      }
    }
  )
)

Prefetching

Prefetch data before it's needed:

const queryClient = useQueryClient()
const eden = useEden()

const handleMouseEnter = () => {
  queryClient.prefetchQuery(
    eden.users({ id: userId }).get.queryOptions()
  )
}

Error Handling

Errors are typed as EdenFetchError: status is the HTTP status code and value is the response body.

const { data, error } = useQuery(eden.users.get.queryOptions())

if (error) {
  if (error.status === 401) {
    return <div>Please log in</div>
  }
  return <div>Request failed with status {error.status}</div>
}

error.value is typed as unknown unless the route declares its error responses. Declare them in Elysia to get typed narrowing by status:

// server
.get('/items/:id', handler, {
  response: {
    200: t.Object({ name: t.String() }),
    404: t.Object({ message: t.String() }),
  },
})
// client
if (error) {
  if (error.status === 404) {
    // error.value is narrowed to { message: string }
    return <div>{error.value.message}</div>
  }
  return <div>Request failed with status {error.status}</div>
}

Avoid rendering error.message: the error extends Error, but its runtime message is String(value) — for JSON bodies that renders as [object Object]. Use status and value instead.

On this page