Askr
Routing & Data

Data

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

  • @askrjs/askr0.0.59

How to use data

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

  1. Import the published @askrjs/askr entrypoint.
  2. Keep data configuration next to the component, route, or server composition root that owns it.
  3. Handle pending, unavailable, cancellation, and failure states, then verify the production path.
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, and invalidate() or queryScope().invalidate() can also be called directly wherever a write happens outside a mutation.