Askr documentation
Routing & Data

Queries and Consistency

Key queries, share results between components, and control how cached data goes stale.

Example

const project = defineQuery({
  key: ({ id }: { id: string }) => 'project:' + id,
  fetch: ({ id, signal }) => api.projects.get(id, { signal }),
});

const result = createQuery(project, { id: projectId });

Define a query

defineQuery() takes a QueryDefinition — a key(input) function that derives a cache key, and a fetch(context) function that does the actual request — and hands back a reusable definition you can pass to createQuery(), serveQuery(), or prefetchQuery(). createQuery() also accepts a plain QueryOptions object directly if you don't need the input/key(input) split. Optional isConsistent() and reconcile() functions on the definition don't gate whether fetched data reaches the cache — freshly fetched data is written to the query's state either way. What they control is what happens next: isConsistent() returning false marks the result stale (consistency: 'stale', staleReason: 'inconsistent') and triggers reconcile(), whose return value decides whether the query automatically retries.

Consistency modes

Instead of a single isLoading flag, a Query<T>'s consistency field is one of 'fresh', 'stale', 'refreshing', or 'pending-write', and each value comes with a matching shape for data, error, and stale flags — a refreshing query still has its previous data available, for instance, while a stale query with an error has staleReason: 'error' and either retains previous data or has data: null when no successful value exists. Reading consistency first tells you which of those shapes you're dealing with before you touch data or error directly.

Query scopes

queryScope(namespace) returns a QueryScope with key() and prefix() helpers that build namespaced cache keys, plus its own invalidate(parts, options) method scoped to that namespace. This is useful once query keys get structured — a scope per resource type keeps key-building consistent and keeps invalidation calls from accidentally matching keys that belong to a different feature.

Refresh behavior

Every Query<T> carries a refresh() method from QueryControls that re-runs the fetch and returns a promise, independent of whatever caused the original fetch. reconcile() doesn't decide whether refreshed data replaces the cached value — it's already written by the time reconcile() runs, and reconcile()'s return value only controls whether the query schedules another automatic retry. An out-of-order response arriving late is instead guarded separately, by generation and abort-controller identity checks that discard a stale in-flight fetch's result once a newer one has superseded it.