Skip to content
Reusable List

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.

  1. URL Search Params

    Search, filters, sort, view, and page all live in the query string.

  2. Shared Query Parser

    parseListQuery() turns raw params into one typed, defaulted query object.

  3. Page-Owned Query Service

    Each example's own query-service.ts filters, sorts, and paginates its records.

  4. Server-Rendered Results

    The Server Component renders the records, pagination links, and active-filter links.

Client interaction path

  1. Search / Filters / Sort / View

    The page-owned toolbar reads the current query from the URL.

  2. useListQueryState

    Builds the next query string from that same parser and config.

  3. 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.

  1. Parse and normalize URL state
  2. Search and filter records
  3. Sort the filtered set
  4. Clamp the requested page to a valid range
  5. Paginate the sorted set and return the served page
  6. 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.