Skip to content

E-Commerce Dashboard ​

A complete e-commerce catalog dashboard example, combining multiple operator families, React pagination, and cache invalidation into one scenario.

Product Dataset ​

A typed Product[] catalog that the following expressions filter:

typescript
interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
  rating: number;
  inStock: boolean;
  tags: string[];
  releasedAt: Date;
}

const products: Product[] = [
  { id: 1, name: 'Mechanical Keyboard', category: 'electronics', price: 129, rating: 4.7, inStock: true, tags: ['wireless', 'mechanical'], releasedAt: new Date('2025-08-12') },
  { id: 2, name: 'Noise-Cancelling Headphones', category: 'electronics', price: 249, rating: 4.8, inStock: true, tags: ['wireless', 'over-ear'], releasedAt: new Date('2025-09-30') },
  { id: 3, name: 'Ergonomic Office Chair', category: 'furniture', price: 399, rating: 4.5, inStock: false, tags: ['mesh', 'adjustable'], releasedAt: new Date('2025-06-03') },
  { id: 4, name: 'Laptop Backpack', category: 'accessories', price: 79, rating: 4.3, inStock: true, tags: ['waterproof', 'travel'], releasedAt: new Date('2025-07-21') },
  { id: 5, name: 'Smart Home Hub', category: 'electronics', price: 149, rating: 4.1, inStock: true, tags: ['wireless', 'smart-home'], releasedAt: new Date('2025-10-15') },
  { id: 6, name: 'Standing Desk', category: 'furniture', price: 599, rating: 4.6, inStock: true, tags: ['adjustable', 'motorized'], releasedAt: new Date('2025-05-11') },
  { id: 7, name: 'Ultrabook Laptop', category: 'electronics', price: 1249, rating: 4.9, inStock: true, tags: ['ultrabook', 'usb-c'], releasedAt: new Date('2025-11-02') },
  { id: 8, name: 'Wireless Mouse', category: 'electronics', price: 49, rating: 4.2, inStock: true, tags: ['wireless', 'compact'], releasedAt: new Date('2025-04-19') },
  { id: 9, name: 'Desk Organizer', category: 'accessories', price: 29, rating: 3.9, inStock: false, tags: ['storage', 'bamboo'], releasedAt: new Date('2025-03-08') },
  { id: 10, name: 'Portable Monitor', category: 'electronics', price: 219, rating: 4.4, inStock: true, tags: ['usb-c', 'portable'], releasedAt: new Date('2025-12-01') },
];

One $and expression combining a price range ($gte/$lte), an array tag match ($contains), and a regular expression on the product name:

typescript
import { filter } from '@mcabreradev/filter';

// Wireless electronics between $100 and $300
filter(products, {
  $and: [
    { price: { $gte: 100 } },
    { price: { $lte: 300 } },
    { tags: { $contains: 'wireless' } }, // any 'wireless' tag
    { name: { $regex: /^(smart|noise)/i } },
  ],
});
// -> [
// ->   { id: 2, name: 'Noise-Cancelling Headphones', price: 249, ... },
// ->   { id: 5, name: 'Smart Home Hub', price: 149, ... }
// -> ]

// Same expression without the name constraint
filter(products, {
  $and: [
    { price: { $gte: 100 } },
    { price: { $lte: 300 } },
    { tags: { $contains: 'wireless' } },
  ],
});
// -> [1, 2, 5] // Mechanical Keyboard, Noise-Cancelling Headphones, Smart Home Hub

Filtering by Date Range ​

Date values are compared by their time value, so a releasedAt bound checked with $gte/$lte uses the same instant semantics as a numeric comparison while remaining null-safe:

typescript
// Products released between 2025-07-01 and 2025-09-30 (inclusive)
const from = new Date('2025-07-01');
const to = new Date('2025-09-30T23:59:59.999');

filter(products, {
  $and: [
    { releasedAt: { $gte: from } },
    { releasedAt: { $lte: to } },
  ],
});
// -> [
// ->   { id: 1, name: 'Mechanical Keyboard', releasedAt: 2025-08-12, ... },
// ->   { id: 2, name: 'Noise-Cancelling Headphones', releasedAt: 2025-09-30, ... },
// ->   { id: 4, name: 'Laptop Backpack', releasedAt: 2025-07-21, ... }
// -> ]

// Dates at the same instant compare as equal
filter([{ t: new Date('2025-07-01T00:00:00Z') }, { t: new Date('2025-07-02T00:00:00Z') }], {
  t: { $eq: new Date('2025-07-01T00:00:00Z') },
});
// -> [{ t: 2025-07-01T00:00:00Z }]

Note: Dates compare by their time value, not by object identity. Two Date instances created for the same instant are treated as equal by the range operators.

Paginated Results with usePaginatedFilter ​

The React binding filters and paginates in one call. currentPage starts at 1, and goToPage/nextPage/previousPage clamp to the valid range:

typescript
import { usePaginatedFilter } from '@mcabreradev/filter/react';

const Dashboard = ({ products }: { products: Product[] }) => {
  const [pageSize, setPageSize] = useState(3);

  const {
    filtered,
    isFiltering,
    currentPage,
    totalItems,
    totalPages,
    nextPage,
    previousPage,
    goToPage,
  } = usePaginatedFilter<Product>(products, { inStock: { $eq: true } }, pageSize);

  // totalItems is the number of matching products (all in-stock items)
  // totalPages is ceil(totalItems / pageSize)
  // currentPage starts at 1; goToPage clamps to [1, totalPages]

  return (
    <div>
      <ProductGrid products={filtered} />
      <Pagination
        current={currentPage}
        total={totalPages}
        onNext={nextPage}
        onPrevious={previousPage}
        onGoTo={goToPage}
      />
      <PageSizeSelector value={pageSize} onChange={setPageSize} />
      {isFiltering && <Spinner />}
    </div>
  );
};

The hook owns the page position and clamps every goToPage/nextPage/previousPage call to the valid range, so you don't keep your own page state — read currentPage and drive navigation with the returned actions. setPageSize resets to page 1.

With the seed dataset, { inStock: { $eq: true } } matches ids [1, 2, 4, 5, 6, 7, 8, 10], so with pageSize of 3:

typescript
// -> currentPage: 1, totalItems: 8, totalPages: 3
// -> page 1: ids [1, 2, 4]   (first 3 in-stock products)
// -> page 2: ids [5, 6, 7]
// -> page 3: ids [8, 10]
// -> goToPage(9)  -> currentPage: 3 (clamped)
// -> previousPage -> currentPage: 2
// -> setPageSize(6) -> currentPage: 1, totalPages: 2

Cache Invalidation After a Data Refresh ​

The filter cache is enabled per-call with the enableCache option. The result cache is keyed on the array reference plus the expression hash — so replacing the dataset with a brand-new array won't serve stale entries. The case that needs clearFilterCache() is mutating the same array reference in place (then re-filtering within the cache TTL), because mutation doesn't change the cache key:

typescript
import { filter, clearFilterCache } from '@mcabreradev/filter';

// A mutable catalog that refreshes in place, keeping the same array reference
const catalog: Product[] = [
  { id: 1, name: 'Mechanical Keyboard', price: 129, tags: ['wireless', 'mechanical'], inStock: true, rating: 4.7, category: 'electronics', releasedAt: new Date('2025-08-12') },
  { id: 2, name: 'Noise-Cancelling Headphones', price: 249, tags: ['wireless', 'over-ear'], inStock: true, rating: 4.8, category: 'electronics', releasedAt: new Date('2025-09-30') },
  { id: 3, name: 'Desk Organizer', price: 29, tags: ['storage', 'bamboo'], inStock: true, rating: 3.9, category: 'accessories', releasedAt: new Date('2025-03-08') },
];

// First call primes the cache with a result keyed to this `catalog` reference
console.log(filter(catalog, { price: { $gte: 200 } }, { enableCache: true }));
// -> [{ id: 2, name: 'Noise-Cancelling Headphones', price: 249, ... }]

// Mutate in place (same reference) and re-filter within the cache TTL
catalog[2].price = 300; // e.g. a price update from a live refresh

// The cache would still return the old entry unless we invalidate it
clearFilterCache();
console.log(filter(catalog, { price: { $gte: 200 } }, { enableCache: true }));
// -> [{ id: 2, ... }, { id: 3, ... }]   // fresh, includes the updated item

Note: clearFilterCache() is imported from the package root (@mcabreradev/filter) and clears both the result cache and the memoized predicate/regex caches. Call it after any in-place mutation of the same array reference; replacing the array with a fresh reference needs no invalidation.

Released under the MIT License.