Reference / Building Blocks
Building Blocks
Templates shows how these compose into four layers. This page turns that around: every primitive in src/features/list-page/ on its own — what it renders, whether it's a Server Component, a Client Component, or a client hook, and which examples actually use it.
TL;DR
The shared list-page core owns the repeatable mechanics: typed URL parsing and serialization, search and filter controls, sorting, view switching, result layouts, pagination, active-filter links, selection primitives, and system states.
Each feature keeps its domain decisions outside that core—record types, mock data, filter options, query services, toolbar composition, rows, cards, status presentation, and bulk behavior. New examples compose these primitives without changing their underlying implementation.
Structure & Results
The blocks that give every list page its outer shape, render its records, and format the metadata inside them — without knowing anything else about what a record contains.
- ListPageShellShared Component
Page-level layout: a title/description header, a slot for the page's own toolbar, the results as children, and an optional pagination slot. It carries no "use client" directive itself, so it renders safely from both a Server Component page.tsx and a Client Component error.tsx.
Used by every example's page.tsx, loading.tsx, and error.tsx (Components, Issues, Deployments, Packages).
- ResultsViewServer Component
Switches between the list and grid layout for a set of items, generic over the item type and delegating each item's markup back to the page through renderListItem and renderGridItem.
Used by Components, Deployments, and Packages results components — Issues renders its own selectable table instead, reusing the exported RESULTS_GRID_CLASS_NAME constant to keep its grid breakpoints identical.
- formatRelativeTimeUniversal
Formats one of the project's date-only ISO strings (e.g. "2026-08-15") into a short, locale-aware relative label such as "3d ago" or "today", by comparing it against today's date.
Used by Components' and Issues' list-row and grid-card renderers.
Query & Navigation
The single parsing boundary that turns URL search params into typed list state and back, plus the client hook that writes to it.
- Query typesUniversal
ViewMode, SortOption, FilterOption, FilterValues, ListQueryConfig, and ParsedListQuery type every example's URL state the same way, so a new example only supplies its own sort keys and filter keys.
Used by every example's config, query service, and toolbar.
- parseListQuery / buildListQueryStringUniversal
parseListQuery reads a URLSearchParams into a ParsedListQuery against a page's ListQueryConfig; buildListQueryString does the reverse. toSearchParams and emptyFilterValues support that same boundary from a Server Component's searchParams object and an empty-state default.
Used by every example's page.tsx, route.ts, and useListQueryState — always against the same config.
- useListQueryStateClient Hook
Reads the current URL with useSearchParams and exposes setSearch, setSort, setView, setSingleFilter, and toggleMultiFilter — each one pushes or replaces a query string built by buildListQueryString. Pagination links are rendered server-side instead, so paging is intentionally not part of this hook.
Used by each example's toolbar client component.
Search, Filters, Sorting & Views
The interactive controls a page-owned toolbar composes together; each one only knows how to read and report one piece of query state.
- SearchFieldClient Component
Debounced text input (300ms default) with a leading search icon; keeps its own draft state so keystrokes don't push a URL update on every character.
Used by every example's toolbar.
- SingleSelectFilterClient Component
A filter that holds at most one value, rendered as a dropdown with a radio group; the trigger always shows the current selection as plain text.
Used by every example's toolbar.
- MultiSelectFilterClient Component
A filter that holds several values at once, rendered as a checkbox popover; the trigger label collapses to "N selected" once more than one value is applied.
Used by Components' framework filter, currently its only consumer.
- SortMenuClient Component
Single dropdown for a page's sort options, generic over the sort key; the active option is always shown as text on the trigger, never an icon alone.
Used by every example's toolbar.
- ViewSwitcherClient Component
Two-option toggle group between list view and grid view.
Used by every example's toolbar.
Pagination & Active Filters
Server-rendered blocks that read query state back out as plain links, so paging and clearing a filter both work without client JavaScript.
- PaginationControlsServer Component
Numbered page links plus Prev/Next, paired with a "Showing 1–20 of 84" range label. Every link is a real Link built from a page-supplied buildHref, and the current page keeps aria-current="page".
Used by every example page.
- ActiveFiltersServer Component
Renders one removable pill per applied filter plus a "Clear all" link, from a list of ActiveFilterPill objects the page builds itself; renders nothing once no filter is active.
Used by every example's results component.
Selection & Bulk Actions
Client-only selection state and the toolbar that surfaces it, kept deliberately outside the shareable URL state above.
- useSelectionClient Hook
Client-only selection state keyed by record id — selectedIds, selectedCount, isSelected, toggle, selectAll, removeMany, clear. A page remounts it, typically via a key derived from the active query, to reset selection when the result set changes.
Used by Issues, currently the only example with row selection.
- SelectionToolbarClient Component
Appears once selectedCount is above zero; shows the count, a "Select all N on this page" action while some visible rows are unchecked, and a page-owned actions slot for the actual bulk operations. A pending flag disables selection changes while a bulk action is in flight.
Used by Issues.
Loading, Empty, Error & Demo-State Recovery
The shared presentation for every state a list can be in, plus the recovery hook and query parameter every mock endpoint uses to simulate them on demand.
- ListSkeletonServer Component
Fixed-height placeholder rows shaped by a page-supplied gridTemplateColumns and columnCount, so the loading state matches the real row layout and nothing shifts when data arrives.
Used by every example's loading.tsx.
- ListEmptyStateServer Component
Icon, one-sentence title and description, and an optional action; a results component supplies its own copy to distinguish "no data at all" from "no results for these filters."
Used by every example's results component.
- ListErrorStateShared Component
Same shape as the empty state, styled for failure, with a required action — always the retry button driven by useDemoErrorRecovery. It carries no "use client" directive itself; every current consumer happens to be a Client Component only because Next.js requires error.tsx itself to opt into the client runtime.
Used by every example's error.tsx (a Client Component).
- useDemoErrorRecoveryClient Hook
Shared by every error.tsx. Clears the ?demoState=error query param to recover from a simulated failure and re-renders once the URL confirms it's gone, or calls the page's own retry() for a genuine thrown error.
Used by every example's error.tsx.
- DemoState helpersUniversal
DemoState and parseDemoState read an explicit ?demoState= query parameter (default, loading, empty, or error) that every mock Route Handler and example page accepts, so any state can be previewed without special tooling; simulateLatency adds an artificial delay to a Route Handler response.
Used by every /api/* route and example page.tsx.
One config, three call sites
A fictional Snapshots example passes the exact same ListQueryConfig to parseListQuery from a Server Component, a Route Handler, and (through useListQueryState) a Client Component — so the three never disagree about what the URL means.
// src/app/examples/snapshots/page.tsx (Server Component)
const searchParams = toSearchParams(await props.searchParams);
const query = parseListQuery<SnapshotSortKey, SnapshotFilterKey>(
searchParams,
SNAPSHOT_LIST_QUERY_CONFIG,
);
// src/app/api/snapshots/route.ts (Route Handler)
const searchParams = new URL(request.url).searchParams;
const query = parseListQuery<SnapshotSortKey, SnapshotFilterKey>(
searchParams,
SNAPSHOT_LIST_QUERY_CONFIG,
);
// src/features/snapshots-example/snapshots-toolbar.tsx (Client Component)
const { query, setSort } = useListQueryState<
SnapshotSortKey,
SnapshotFilterKey
>(SNAPSHOT_LIST_QUERY_CONFIG);What stays page-owned
Item renderers (list rows, grid cards), query services, mock data, filter option lists, and the toolbar that wires the controls above together all live in each example's own feature folder — for instance src/features/components-example/ — never in the shared src/features/list-page/ core. This page catalogs the primitives on their own; how they compose into a page is Templates.