Back to System Design
System Design
hard
mid

How would you design the frontend for a product listing page including components, state, data flow, and scalability?

Page composes `<Filters>`, `<ProductGrid>`, `<Pagination>`. URL is the source of truth for filters/sort/page (shareable, back-button safe). Server-side fetch + SSR/streaming for SEO; React Query handles client refetches on filter change. Skeleton states, optimistic UI on add-to-cart, image lazy-load + responsive srcset. A11y: filter region landmarks, announce result count.

5 min read·~30 min to think through

A product listing page (PLP) is the canonical "list + filters" frontend system design. The interesting parts are URL state, rendering strategy, filter ergonomics, and perf at scale.

Component tree

ts
<ProductListingPage>
  <Breadcrumb />
  <FiltersSidebar>
    <Filter name="category" />
    <Filter name="price" />
    <Filter name="brand" />
  </FiltersSidebar>
  <Toolbar>
    <ResultCount />
    <SortSelect />
    <ViewToggle />
  </Toolbar>
  <ProductGrid>
    <ProductCard />
    ...
  </ProductGrid>
  <Pagination />
</ProductListingPage>

State

StateLocation
Filters / sort / pageURL (?category=shoes&sort=price&page=2) — shareable, back-button.
Products dataReact Query (cached, refetched on URL change).
UI ephemeral (drawer open, hover)local useState.
Cartglobal (Zustand or Context).

URL as source of truth is the single most important decision — it makes the page shareable, back-button safe, and SSR-able.

Rendering strategy

  • SSR for SEO + LCP. Server renders the first page with current URL filters.
  • Client refetch on filter change (URL update → React Query refetch).
  • Streaming if filter facets or related data load slowly — render grid first, facets next.
  • RSC for non-interactive parts (breadcrumb, footer) to reduce JS.

Data flow

ts
URL change → useSearchParams → query key changes → React Query refetch → grid re-renders

Debounce the URL update for sliders / text filters (e.g. price range) to avoid refetch-per-pixel.

Filter ergonomics

  • Multi-select with chip display.
  • Apply on change for fast filters; Apply button for slow / mobile.
  • Clear all + per-filter clear.
  • Result count updates live ("142 results"); announced with aria-live="polite".
  • Counts per option ("Red (23)") help discoverability — facet counts from API.

Grid

  • CSS Grid: grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)).
  • Skeleton cards during loading; not a spinner over the whole grid.
  • Image: <img loading="lazy" srcset sizes>; AVIF/WebP with JPEG fallback.

Pagination

  • Cursor pagination for big catalogs (avoid OFFSET drift).
  • Or "Load more" / infinite scroll with intersection observer (but keep a Pagination fallback for SEO).

Performance levers

  • Lazy load images below the fold.
  • Above-the-fold critical CSS inlined.
  • Prefetch likely-next page on hover.
  • Cache product detail data so PDP loads instantly on click.
  • Image CDN with ?w=400&q=70&fm=avif transforms.

A11y

  • <aside aria-label="Filters"> landmark.
  • Filter group: <fieldset><legend>.
  • Result count: aria-live="polite".
  • Pagination as a labeled <nav> with aria-current="page".
  • Skeleton has aria-busy="true" on parent.

Edge cases

  • Empty filter combos → empty state with "Clear filters" CTA.
  • Out-of-stock items — filter or fade them.
  • Search + filters together → combine query keys.
  • Locale / currency — formatted from user settings.

Interview framing

"URL is the source of truth for filters/sort/page — shareable, back-button safe, SSR-ready. React Query handles the data layer keyed by URL params. Server renders the first page for SEO + LCP; subsequent navigations are client-side. Filters with live counts, skeleton grid for loading, lazy-load images with responsive srcset. Use CSS Grid auto-fit for the layout. Announce result counts via aria-live. Performance levers: image CDN, prefetch on hover, cache product detail. The biggest mistake is putting filter state in local React state and losing it on back navigation."

Follow-up questions

  • Why URL over local state for filters?
  • Compare offset vs cursor pagination.
  • How do you handle infinite scroll a11y?

Common mistakes

  • Filters in component state — back button breaks.
  • Re-rendering all cards on filter change.
  • No skeletons → blank during refetch.
  • Image LCP killed by lazy-loading hero images.

Performance considerations

  • Image CDN + AVIF/WebP, lazy-load below fold, prefetch hover, cache PDP, SSR first page.

Edge cases

  • URL-encoded weird filter values.
  • Filter combinations with zero results.
  • Mobile filter drawer vs desktop sidebar.

Real-world examples

  • Amazon, Shopify, ASOS, eBay; Algolia + InstantSearch reference impl.

Senior engineer discussion

Seniors choose URL-as-state, design rendering per section (SSR / RSC / client), and optimize the image pipeline aggressively — images are 70%+ of bytes on PLPs.

Related questions