Loading, Empty, Error, and Pending States
Use loading, empty, error, and pending states with the current published Askr packages.
@askrjs/askr0.0.59
How to use loading, empty, error, and pending states
Render pending, empty, and recoverable failure states at the query boundary while preserving the last useful context during refreshes.
- Import the published @askrjs/askr entrypoint.
- Keep loading, empty, error, and pending states configuration next to the component, route, or server composition root that owns it.
- Handle pending, unavailable, cancellation, and failure states, then verify the production path.
import { Show } from '@askrjs/askr';
<Show when={!projects.pending} fallback={<ProjectSkeleton />}>
<Show when={!projects.error} fallback={<ProjectError onRetry={projects.refetch} />}>
<Show when={projects.data.length > 0} fallback={<EmptyProjects />}>
<ProjectList projects={projects.data} />
</Show>
</Show>
</Show>Goal and architecture
Askr doesn't ship a single 'AsyncBoundary' component — loading, empty, and error rendering are ordinary conditionals over whatever async primitive produced the data. If that's a route loader, use `defer()` and `<Resolve>` so a slow promise doesn't block navigation; if it's a query, its `consistency` field ('fresh' | 'stale' | 'refreshing' | 'pending-write') and stale reason tell you exactly what's happening. Treat 'no data yet' (loading), 'data came back empty' (empty), and 'the request failed' (error) as three distinct branches in your JSX rather than one catch-all spinner-or-content toggle.
Implementation
For deferred route data, wrap the value in `<Resolve>` and use its pending/rejected render paths — `isDeferred(value)` lets shared components detect a still-pending `Deferred<T>` versus a resolved value. For query-backed views, read `refreshing` and `consistency` off the query object to show a subtle in-place indicator during a background refresh instead of tearing down the whole view; reserve the `@askrjs/ui` `Progress`/`ProgressCircle` components for genuine first-load states. Render the empty state as a real branch (`data.length === 0`) rather than letting an empty array fall through to whatever your list component does with nothing.
Failure states
A query's stale reason distinguishes `'aborted'`, `'error'`, and `'inconsistent'` — only the `'error'` case should render an error UI; the others usually mean 'still show the last good data, just mark it stale.' Pair this with `@askrjs/fetch`'s result `kind` on the request that populated the query so an HTTP 404 renders 'not found' and a timeout renders 'retry', instead of one generic failure card. Surface transient failures (a background refresh that failed but you still have old data) as a dismissible `Toast`, not a full-page error, since the user hasn't actually lost anything yet.
Verification
`@askrjs/askr/testing` ships `mockQuery`/`queryState` helpers with `.loading()`, `.error(error, previousData)`, `.refreshing(data)`, and `.stale(data, reason)` constructors, so you can render each branch of a component against a synthetic query without touching the network. Write one test per branch — loading, empty, error, refreshing-with-stale-data — rather than one test that only checks the fresh-data case. For deferred loaders, resolve and reject the underlying promise in tests and assert `<Resolve>` renders the right branch for each outcome.