Askr documentation
Routing & Data

Data

Choose resources, queries, mutations, and actions by ownership and consistency needs.

Example

Define the cache key and cancellable fetch contract once, then create the query where a component needs the result.

import { createQuery, defineQuery } from '@askrjs/askr/data';

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

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

Primitive selection

Askr splits data handling into four primitives instead of one do-everything hook: plain state() for values only a component cares about, resource() for one-off async work tied to a component's lifecycle, createQuery()/defineQuery() for server data shared across the app, and createMutation()/action() for writes. Picking the right one comes down to two questions: does more than one part of the tree need this data, and does a write need to invalidate it elsewhere? If the answer to both is no, reach for resource() or plain state() before anything from the data module.

Local lifecycle work

resource() from @askrjs/askr/resources runs an async function scoped to wherever you call it, and re-runs whenever its dependency array changes, the same way an effect dependency list works. It returns pending, error, and value fields directly, so a component can branch on them without extra wiring. Because the fetch function is passed a signal, in-flight work is cancelled automatically when dependencies change or the component unmounts.

Shared server data

Once two or more components need the same server data, a resource() call per component means duplicate requests and no shared cache. createQuery() and defineQuery() solve that by keying data on a string derived from the input, so repeated calls with the same key reuse the same cached entry through a DataRuntime. Consistency is tracked explicitly through states like fresh, stale, refreshing, and pending-write rather than a single boolean loading flag.

Writes and invalidation

Writes go through createMutation(), which wraps an async action function and exposes execute(), abort(), and reset() alongside a status field (idle, pending, success, or error). A mutation's affects() option maps its input and result to the query key prefixes it should invalidate — gated by afterSuccess: 'invalidate', which is what actually triggers the invalidation, not affects() being present on its own — and invalidate() or queryScope().invalidate() can also be called directly wherever a write happens outside a mutation.