Askr documentation
Routing & Data

Loaders and Deferred Values

Load route data before render, and mark the slow parts deferred so they do not block the first paint.

Example

Return critical loader data directly and wrap slower independent work in defer so the page can stream a stable shell.

import { defer } from '@askrjs/askr/router';

route('/projects/{projectId}', ProjectPage, {
  loader: async ({ params }) => ({
    project: await projects.get(params.projectId),
    activity: defer(activity.list(params.projectId)),
  }),
});

Route loaders

RouteOptions.loader is a function that receives the route's RouteContext plus an optional request, and its return value becomes the data available to the route on first render — including during SSR, where it runs before the response streams. preload is a separate hook meant for warming query caches ahead of render rather than blocking on the result, and it's handed a QueryPrefetchContext alongside the usual context fields. Both run per-navigation, keyed to the route that's about to become active, not on some global schedule.

Deferred values

defer(promise) wraps a PromiseLike<T> into a Deferred<T> you can return from a loader without blocking the initial render on it — the page renders immediately and the deferred value resolves in the background. isDeferred() checks whether a given value is one of these wrappers, which loaders and data-fetching code use to decide whether to await it directly or hand it to Resolve. routeData<T>() reads whatever the current route's loader returned, deferred values included, from inside the rendering component.

Pending and rejected states

A Deferred<T> exposes its own state ('pending' | 'fulfilled' | 'rejected'), plus value and error fields that update as the underlying promise settles. The Resolve component is the declarative way to render against that state: give it a value, a pending fallback, an optional rejected fallback (a node or a function of the error), and a children render function that only runs once the value is fulfilled. That keeps loading and error UI colocated with the data they describe instead of scattered across manual if checks.

Cancellation

Every loader and preload call gets a signal: AbortSignal on its context, and Askr aborts it automatically if a newer navigation supersedes the one that triggered it — so a slow loader for a route the user already navigated away from doesn't finish late and overwrite current state. resolveDeferredValues(input, signal) walks a data structure and resolves any nested Deferred values it finds, respecting that same signal so a whole tree of pending data can be cancelled together rather than one promise at a time.