query() connects your UI to your API with reactive data fetching, caching, and automatic cleanup.
Basic usage
query() accepts a query descriptor (from the typed API client) or a thunk that returns a promise:
Reactive properties
A query result exposes five reactive properties:| Property | Type | Description |
|---|---|---|
data | T | undefined | The fetched data, or undefined while loading |
loading | boolean | true only on initial load (not during revalidation) |
revalidating | boolean | true when refetching with existing data |
error | Error | undefined | The error from the last failed fetch |
idle | boolean | true when the query has never fetched (thunk returned null) |
Reactive dependencies
When the thunk reads a signal, the query automatically re-fetches when that signal changes:await in the thunk becomes a dependency.
Loading related data
Useinclude to fetch related data in a single query instead of making separate API calls and joining them client-side:
expose.include config on the server — see Fields, Relations & Filters.
Rendering query states
Use direct conditional rendering for loading, error, and data states:tasks.loading, tasks.error, and tasks.data as signal properties, making each branch reactive.
Refetching
Callrefetch() to manually re-fetch:
Polling
UserefetchInterval to poll at a fixed interval:
Conditional queries
Sincequery() is not a hook, you can call it conditionally. But when you need a query that starts idle and fetches later based on a reactive condition, return null from the thunk to skip fetching:
null, the query stays idle — loading is false, data is undefined, and idle is true. As soon as the reactive dependency changes and the thunk returns a real value, the query fetches automatically:
idle property distinguishes “hasn’t fetched yet” from “fetched but got no results” — useful for showing different UI states:
Dependent query chains
Use null-return queries to chain dependent fetches — a query that depends on another query’s result:idle is a one-way flag — once the query fetches for the first time, idle becomes false and
never returns to true, even if the thunk returns null again later. This prevents UI flicker
when conditions change temporarily.Options
Caching
Queries are cached in a shared in-memory cache. For entity-backed queries (from the typed API client), the cache key is derived from the HTTP method, path, and query parameters:api.tasks.list()→GET:/api/tasksapi.tasks.get('abc')→GET:/api/tasks/abcapi.tasks.list({ status: 'done' })→GET:/api/tasks?status=done
{ status: 'done', page: 1 } and { page: 1, status: 'done' } produce the same key.
For plain thunks (not using the API client), the cache key is derived from the function body and the reactive signal values it reads. Returning to a previously-seen set of dependency values produces the same cache key — enabling cache hits without re-fetching.
Stale-while-revalidate
When you navigate away and back, cached data is served instantly while a background revalidation runs. The user sees content immediately — no loading spinner for data that was already fetched.Disposal preserves cache
Queries are auto-disposed when their component unmounts — no manual cleanup needed. Disposal stops reactive effects and timers but preserves the shared cache, so navigating back serves data instantly.Auto-revalidation
When an entity mutation completes (create, update, delete), all active queries for that entity type are automatically revalidated in the background. No configuration needed — the mutation event bus handles it:Manual invalidation
For edge cases not covered by automatic revalidation (e.g., a custom server-side operation that affects entity data), useinvalidate():
invalidate() matches by entity type and operation kind, not by cache key. This means invalidate(api.todos.list()) revalidates ALL active todo list queries — including query(api.todos.list({ status: 'done' })) and query(api.todos.list({ assignee: userId })). You don’t need to invalidate each filter variation individually.
Existing data stays visible while the refetch happens in the background (SWR pattern).
invalidate() is an escape hatch. In most cases, automatic revalidation from entity mutations
handles everything. Use it only for operations outside the standard entity CRUD flow.Tenant-scoped invalidation
When using multi-tenancy,switchTenant() automatically invalidates all tenant-scoped queries (clearing cached data before refetching). If you need to manually trigger this:
invalidate(), this clears cached data immediately (no SWR stale window) — users never see data from the wrong tenant.
Optimistic updates
Entity mutations automatically apply optimistic updates — the UI reflects changes instantly while the server request is in flight:- Applies the mutation optimistically to all queries that reference the entity
- Commits when the server confirms success
- Rolls back if the server returns an error
How it works
Entity data is stored in a normalizedEntityStore. When a mutation fires:
- An optimistic layer is pushed onto the store with the pending changes
- All entity-backed queries read through the layer stack, seeing the optimistic state
- On server success, the layer is committed (merged into base data)
- On server error, the layer is rolled back (UI reverts to server truth)
create, update, delete) on generated SDK methods.
SSR data loading
query() automatically resolves data during SSR so the page arrives with content — no loading flash:
- Pass 1 (discovery) — The server renders your app, triggering
query()calls. Each query registers its fetch promise. - Await — The server waits for all queries to resolve (default 300ms timeout per query).
- Pass 2 (render) — The server renders again with cached data. Queries serve from cache, so
loadingisfalse. - Hydration — The client picks up the streamed data from
window.__VERTZ_SSR_DATA__and skips the initial fetch.
Configuration
Auto field selection
The Vertz compiler automatically analyzes which fields your components access on query results and injects aselect parameter — so the server only returns the columns you actually use. This works transparently for components within your codebase. For third-party npm components, you can optimize by narrowing the data you pass across the boundary.
Auto Field Selection
How auto field selection works, what triggers fallback to all fields, and how to optimize at
third-party component boundaries.