Askr
Guides

Tables, Filters, and URL State

Use tables, filters, and url state with the current published Askr packages.

  • @askrjs/askr0.0.59

How to use tables, filters, and url state

Store shareable sort, filter, and page state in the URL; derive the query input from that state and render rows with stable keys.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep tables, filters, and url state 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 { currentRoute, updateRouteQuery } from '@askrjs/askr/router';

const route = currentRoute();
const filters = () => ({
  status: route.query.status ?? 'active',
  page: Number(route.query.page ?? 1),
});

function setStatus(status: string) {
  updateRouteQuery({ status, page: 1 });
}

Goal and architecture

There's no headless table primitive with built-in sort/filter/paginate logic in this framework — `@askrjs/ui`'s `Table`, `TableHead`, `TableBody`, `TableRow`, `TableHeaderCell`, and `TableCell` are presentational, styled components, not a data grid. Filter and sort state for a real table screen belongs in the URL, driven through the router's `updateRouteQuery()` and read back with the route's search params, with a `createQuery` refetching whenever that state changes.

Implementation

Call `updateRouteQuery({ sort: 'name', page: 2 })` to push new query params, or pass an updater function `(searchParams) => { ... }` from `RouteQueryUpdater` when you need to read the existing params before changing them; `UpdateRouteQueryOptions` controls whether the change pushes a new history entry or replaces the current one. Read the current filter/sort state back from the route's search object and pass it as part of your `createQuery` key, so changing a filter naturally produces a new cache entry and refetch instead of you managing a separate `useEffect`-style dependency list. Render the results into `Table`/`TableRow`/`TableCell` — they're just markup, so pagination and empty-state handling are your component's job, not the table's.

Failure states

An invalid or out-of-range filter value in the URL — a `page` beyond the last page, an unrecognized `sort` key — should clamp or fall back to a sane default rather than rendering an empty table with no explanation, since users routinely land on these URLs from bookmarks or shared links. A query that errors for a specific filter combination needs its own message; a bare `data.error` check without inspecting what actually failed will make every failure look identical to the user. Repeated keys in the query string become `string[]` on the server side (via `ctx.bind()`) — if you're also reading query params server-side for the same route, make sure both sides agree on that shape.

Verification

Change a filter, then use the browser back button, and confirm the table reflects the previous filter state rather than the current one — this is the entire point of putting the state in the URL instead of a component. Load the URL fresh with filter params already present (not by clicking through the UI) and confirm the table renders correctly on first paint, not just after a user interaction triggers a refetch. Finally check that two different filter combinations produce two distinct query cache entries rather than one clobbering the other.