Real World Example

A Filterable Listing Like Myntra

Myntra's listing page keeps brand, price, size and sort in the URL (/shoes?brand=nike,puma&sort=price_asc). The server renders results from the URL; checkboxes in the sidebar just change the URL. ShopEase builds the same thing:

// app/products/page.tsx (server)
import Filters from './Filters'; // 'use client' sidebar that updates the URL
import { searchProducts } from '@/lib/products';
import { parseFilters } from '@/lib/filters';

export default async function ProductsPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const filters = parseFilters(await searchParams);
  const { items, total } = await searchProducts(filters);
  return (
    <div className="grid md:grid-cols-[260px_1fr] gap-6">
      <Filters />
      <section>
        <p>{total} results</p>
        {items.map((p) => (
          <div key={p.id}>{p.name}</div>
        ))}
      </section>
    </div>
  );
}

Toggling a Brand

Example

The sidebar helper that adds or removes one brand from the query string:

function toggleParam(query: string, key: string, value: string): string {
  const params = new URLSearchParams(query);
  const values = params.getAll(key);
  params.delete(key);
  const next = values.includes(value) ? values.filter((v) => v !== value) : [...values, value];
  next.sort().forEach((v) => params.append(key, v));
  params.delete('page');
  const s = params.toString();
  return s ? '?' + s : '';
}

let url = '?sort=price_asc&page=3';
for (const brand of ['puma', 'nike', 'puma']) {
  url = toggleParam(url.slice(1), 'brand', brand);
  console.log('toggle ' + brand.padEnd(5) + ' -> /products' + url);
}

Output

toggle puma  -> /products?sort=price_asc&brand=puma
toggle nike  -> /products?sort=price_asc&brand=nike&brand=puma
toggle puma  -> /products?sort=price_asc&brand=nike