# Tables, Filters, and URL State

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

Source: [https://askrjs.com/docs/guides/tables-filters-and-url-state](https://askrjs.com/docs/guides/tables-filters-and-url-state)

Status: stable. Packages: @askrjs/askr.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

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

```tsx
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.

## Documentation navigation

[Previous](https://askrjs.com/docs/guides/forms-actions-and-crud/index.md) | [Next](https://askrjs.com/docs/guides/dashboards-charts-and-polling/index.md)
