Reference / Architecture
Architecture
A URL-driven list-page system with one parsing boundary, server-rendered results, focused client controls, and page-owned domain logic.
TL;DR
The URL is the shareable source of truth for search, filters, sorting, view, and pagination. Server Components parse that state, call a page-owned query service, and render the resulting records; focused Client Components only update the URL or manage temporary interaction state.
Each page and its mock Route Handler reuse the same query configuration and domain query function. Selection is the deliberate exception: it stays local to the client because it is temporary UI state rather than shareable list state.
Request-to-render flow
An example page (page.tsx) pulls its list state from the URL and pushes it through the same two steps below before rendering results on the server.
URL Search Params
Search, filters, sort, view, and page all live in the query string.
Shared Query Parser
parseListQuery() turns raw params into one typed, defaulted query object.
Page-Owned Query Service
Each example's own query-service.ts filters, sorts, and paginates its records.
Server-Rendered Results
The Server Component renders the records, pagination links, and active-filter links.
Client interaction path
Search / Filters / Sort / View
The page-owned toolbar reads the current query from the URL.
useListQueryState
Builds the next query string from that same parser and config.
Updated URL
Navigating to it re-renders the server flow above with the new query.
Feeds back into URL Search Params at the top of this diagram.
- URL is the shareable source of truth for the whole list.
- The same query config is used by the page, the Route Handler, and the client toolbar.
- Search replaces the current URL entry; filters, sort, view, and pagination each push a new, navigable one.
- Filtering always runs before sorting, and sorting always runs before pagination.
Server and client boundaries
Most of a list page is plain server rendering. Client Components only take over at the points where a person is actively interacting.
Server-owned
- Parse search params
- Query mock data
- Redirect invalid pages
- Render results
- Render pagination links
- Render active-filter removal links
Client-owned
- Draft search input
- Filter, sort, and view controls
- Selection state
- Bulk-action pending state
- Error recovery interaction
Client Components stay at interaction boundaries; list data is not fetched again by the page in the browser.
Shared core vs. page-owned code
Every example imports the same shared primitives and composes them with its own domain code — it never forks or edits the shared core.
Shared list-page core
src/features/list-page/- Query parsing and serialization
- Search and filter controls
- Sorting and view switching
- Results layout
- Pagination
- Active filters
- Selection primitives
- Loading, empty, and error states
Page-owned feature
src/features/issues-example/- Types and filter options
- Mock records
- Query service
- Toolbar composition
- List rows and grid cards
- Status and domain presentation
- Bulk-action behavior, where applicable
Page-owned features compose the shared core through its exported building blocks — they never modify it directly.
Data-processing pipeline
A page request runs through six steps, in order. The middle four run inside the page-owned query service — shared by the matching Route Handler — and only the page performs the final step, comparing the served page to the requested one and redirecting when they differ.
- Parse and normalize URL state
- Search and filter records
- Sort the filtered set
- Clamp the requested page to a valid range
- Paginate the sorted set and return the served page
- Compare the served page to the requested page, then redirect if they differ
API parity
An example page and its mock Route Handler are two entry points into the same query logic, not two independent implementations of it.
page.tsx
Server Component
route.ts
Route Handler
query-service.ts
Filters, sorts, and paginates the mock dataset
The page does not fetch its own Route Handler. Both entry points reuse the same domain query function directly.
Selection is a deliberate exception
Issues is the only example with row selection, and it intentionally breaks from the URL-first rule above.
- Selection is local client state, kept out of the URL.
- It resets whenever the search, filters, sort, or page changes — switching between list and grid view does not reset it.
- Select-all is scoped to the issues visible on the current page.
- A successful bulk status update applies locally, without a full data refetch — nothing is persisted server-side.
- A failed update leaves the current selection in place so it can be retried.