Askr documentation
Guides

Tables, Filters, and URL State

Tables, Filters, and URL State: a worked guide from route registry through to a production build.

Example

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

import { currentRoute, updateRouteQuery } from '@askrjs/askr/router';

const route = currentRoute();
const filters = () => ({
  status: route.query.get('status') ?? 'active',
  page: Number(route.query.get('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.