Skip to the solution
HardMachine Coding14 min read

Build an E-commerce Product Filter grid

1.2k views

The problem

Question presented to candidate: "We have a product catalogue in memory. Build the listing page: a grid of products with a filter sidebar (category checkboxes, a price range, a minimum rating and an in-stock toggle) and a sort dropdown. Next to each filter option, show how many products you would get if you picked it. Show the active filters as removable chips, the result count, and a helpful empty state. A shared link should open the same filtered view."

Requirements:

  • Facets: categories (multi-select, OR within the facet), min and max price (validated), minimum rating (radio), in stock only; facets combine with AND.
  • Sort: most popular, price low to high, price high to low, top rated; ties keep catalogue order.
  • Live facet counts that answer "what would I get if I picked this option", given every other active filter.
  • Active filter chips with remove buttons, "Clear all" (which keeps the chosen sort), a result count announced to screen readers, and an empty state with a clear action.
  • URL sync with history.replaceState (?cat=Audio,Cameras&min=100&rating=4&stock=1&sort=price-asc), parsed and validated on load.
  • Responsive: sidebar beside the grid on desktop, stacked above it on a phone.

What a strong answer should cover:

  • A single filter state object as the source of truth; results, counts, chips and the price error are all derived from it.
  • A composable filter pipeline: one predicate per facet, combined with AND, so adding a facet is one line.
  • Disjunctive facet counts: each facet's counts apply every filter except that facet's own, otherwise selecting "Audio" would show 0 for every other category.
  • Memoising the derivation, and an honest note about when that matters (thousands of products, or server-side search).
  • Validation of user input (min above max, negative numbers) without throwing away what the user typed.
  • Accessibility: fieldset and legend per facet, labelled inputs, chip buttons with "Remove filter" labels, an aria-live count.

Clarifying questions expected:

  • "Is filtering done in the browser over the whole catalogue, or by an API with pagination?"
  • "Within one facet, is it OR (Audio or Cameras) and across facets AND?"
  • "Should the URL push history entries so Back undoes a filter, or replace them?"

Code / implementation expected: Yes. A working page in React, Vue or Angular with the filter logic in plain functions, facet counts, chips, sorting and URL sync, over an in-file product list.

  • React
  • Filters
  • Data

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

14 min read ยท 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { useEffect, useMemo, useState } from "react";
import FilterPanel, { RATINGS } from "./src/components/FilterPanel";
import { CATEGORIES, PRODUCTS } from "./src/data/products";
import {
  EMPTY,
  SORTS,
  activeChips,
  applyFilters,
  facetCounts,
  fromQuery,
  priceError,
  removeChip,
  toQuery,
} from "./src/utils/filtering";

export default function App() {
  // One object holds every filter, read from the URL once on load.
  const [filters, setFilters] = useState(() =>
    fromQuery(window.location.search, CATEGORIES),
  );

  // Everything below is derived from filters; nothing else is state.
  const results = useMemo(() => applyFilters(PRODUCTS, filters), [filters]);
  const counts = useMemo(
    () => facetCounts(PRODUCTS, filters, CATEGORIES, RATINGS),
    [filters],
  );
  const chips = activeChips(filters);
  const error = priceError(filters);

  useEffect(() => {
    window.history.replaceState(
      null,
      "",
      toQuery(filters) || window.location.pathname,
    );
  }, [filters]);

  const clearAll = () => setFilters({ ...EMPTY, sort: filters.sort });

  return (
    <main className="shop">
      <h1>Shop</h1>
      <div className="layout">
        <FilterPanel
          filters={filters}
          counts={counts}
          error={error}
          onChange={setFilters}
        />

        <section aria-labelledby="results-heading">
          <div className="topbar">
            <h2 id="results-heading" data-testid="count" aria-live="polite">
              {results.length} {results.length === 1 ? "product" : "products"}
            </h2>
            <label>
              Sort by{" "}
              <select
                data-testid="sort"
                value={filters.sort}
                onChange={(e) =>
                  setFilters({ ...filters, sort: e.target.value })
                }
              >
                {Object.entries(SORTS).map(([key, s]) => (
                  <option key={key} value={key}>
                    {s.label}
                  </option>
                ))}
              </select>
            </label>
          </div>

          {chips.length > 0 && (
            <ul className="chips" aria-label="Active filters">
              {chips.map((chip) => (
                <li key={chip.id}>
                  <button
                    type="button"
                    data-chip={chip.id}
                    aria-label={"Remove filter " + chip.label}
                    onClick={() => setFilters(removeChip(filters, chip.id))}
                  >
                    {chip.label} โœ•
                  </button>
                </li>
              ))}
              <li>
                <button
                  type="button"
                  data-testid="clear"
                  className="link"
                  onClick={clearAll}
                >
                  Clear all
                </button>
              </li>
            </ul>
          )}

          {results.length === 0 ? (
            <div data-testid="empty" className="empty">
              <p>No products match these filters.</p>
              <button type="button" onClick={clearAll}>
                Clear all filters
              </button>
            </div>
          ) : (
            <ul className="grid">
              {results.map((p) => (
                <li key={p.id} data-testid="product" className="card">
                  <h3>{p.name}</h3>
                  <p className="meta">{p.category}</p>
                  <p className="price">${p.price.toLocaleString("en-US")}</p>
                  <p className="meta">
                    โ˜… {p.rating.toFixed(1)} ยท{" "}
                    {p.reviews.toLocaleString("en-US")} reviews
                  </p>
                  {!p.inStock && <p className="badge">Out of stock</p>}
                </li>
              ))}
            </ul>
          )}
        </section>
      </div>
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 78 of 107 in Machine coding

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

Build a File Explorer with Nested Drag-and-Drop

Continue