Skip to main content

Product Listing Page Design and Implementation

Comprehensive low-level design for e-commerce product listing page with filtering, sorting, and responsive grid layout

This document presents a production-ready low-level design for an e-commerce product listing page, covering component architecture, state management, performance optimizations, and responsive layout patterns.

1. Problem Statement & Requirements

Clarifying questions

  1. What's the target browser support matrix? (e.g., IE11, mobile Safari, Chrome 80+)
  2. Do we need real-time product updates or is static data sufficient?
  3. Should the PLP support offline browsing with cached results?
  4. What's the expected accessibility level? (e.g., WCAG 2.1 AA compliance required?)
  5. Is i18n support needed for international markets or just English?
  6. Are there existing design system components we should leverage?
  7. What's the expected scale of products (e.g., 10k, 100k, 1M+ items)?

Functional requirements

  • Display products in a grid or list view with configurable layout
  • Support filtering by category, price range, brand, and other attributes
  • Support sorting by relevance, price, rating, and release date
  • Show product thumbnails, names, prices, ratings, and availability status
  • Implement infinite scroll or pagination for large result sets
  • Allow users to add products to cart or wishlists
  • Support search functionality with auto-suggestions
  • Display product details on click (modal or new page)
  • Show loading states during data fetches
  • Handle empty search results gracefully

Non-functional requirements

  • Performance: LCP < 2.5s, FID < 200ms, CLS < 0.1
  • Accessibility: WCAG 2.1 AA compliance for all interactive elements
  • Scalability: Support 10k+ concurrent users with sub-second response times
  • Responsiveness: Mobile-first design with adaptive grid layouts
  • Reliability: Graceful degradation when API calls fail

Constraints and assumptions

  • Products are fetched from a remote API with pagination support
  • UI assumes existing design system with consistent spacing, typography, and color palette
  • Authentication handled at the application level (user session available)
  • No server-side rendering required for this feature
  • Mobile devices have at least 320px width available

Out of scope

  • Product creation/editing workflows (admin functionality)
  • Advanced recommendation algorithms or personalization
  • Multi-language product descriptions and metadata
  • Complex filtering like faceted search with nested categories
  • Integration with external services (e.g., price comparison, reviews)
  • Server-side rendering or static site generation
  • Advanced analytics tracking beyond basic event logging

2. Component Architecture

graph TD
    A[ProductListingPage] --> B[ProductFilters]
    A --> C[ProductGrid]
    A --> D[ProductSort]
    A --> E[ProductPagination]
    B --> F[CategoryFilter]
    B --> G[PriceRangeFilter]
    B --> H[BrandFilter]
    C --> I[ProductCard]
    I --> J[ProductImage]
    I --> K[ProductTitle]
    I --> L[ProductPrice]
    I --> M[ProductRating]
    D --> N[SortOptions]
    E --> O[PageControls]
    E --> P[ResultsCount]

Component Tree Breakdown

Smart Components (Container Components):

  • ProductListingPage - orchestrates the entire listing flow
  • ProductFilters - manages filter state and applies filters to product data
  • ProductGrid - renders the grid of products with virtualization support

Dumb Components (Presentational Components):

  • ProductCard - displays individual product with image, title, price, rating
  • CategoryFilter, PriceRangeFilter, BrandFilter - filter UI controls
  • SortOptions - dropdown or button group for sorting options
  • PageControls - pagination navigation controls
  • ProductImage, ProductTitle, ProductPrice, ProductRating - atomic product display elements

TypeScript Props Contracts

// ProductListingPage.tsx
interface ProductListingPageProps {
  initialFilters?: FilterState;
  initialSort?: SortOption;
  initialPage?: number;
}

// ProductFilters.tsx
interface ProductFiltersProps {
  filters: FilterState;
  onFilterChange: (filters: FilterState) => void;
  onResetFilters: () => void;
}

interface FilterState {
  categories: string[];
  brands: string[];
  priceRange: { min: number; max: number };
  searchTerm: string;
}

// ProductCard.tsx
interface ProductCardProps {
  product: Product;
  onClick?: () => void;
}

// ProductGrid.tsx
interface ProductGridProps {
  products: Product[];
  isLoading: boolean;
  hasError: boolean;
  onProductClick: (product: Product) => void;
}

// ProductImage.tsx
interface ProductImageProps {
  src: string;
  alt: string;
  className?: string;
}

// ProductSort.tsx
interface ProductSortProps {
  currentSort: SortOption;
  onSortChange: (sort: SortOption) => void;
}

// ProductPagination.tsx
interface ProductPaginationProps {
  currentPage: number;
  totalPages: number;
  onPageChange: (page: number) => void;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Loading : fetchProducts()
    Loading --> Success : onSuccess()
    Loading --> Error : onError()
    Success --> Idle : resetFilters()
    Success --> Loading : applyFilters()
    Error --> Idle : retryFetch()
    Error --> Loading : retryFetch()

Component Responsibilities

ProductListingPage:

  • Coordinates data fetching and state management
  • Handles URL parameter synchronization
  • Manages the overall page lifecycle and error states

ProductFilters:

  • Maintains filter state (categories, brands, price range)
  • Applies filters to product data
  • Provides reset functionality

ProductGrid:

  • Renders product cards in a responsive grid
  • Implements virtualization for performance
  • Handles loading and error states

ProductCard:

  • Displays individual product details
  • Implements click-to-product navigation
  • Renders all product metadata (image, title, price, rating)

Reusability Decisions

  • ProductCard is highly reusable across different product contexts
  • ProductImage, ProductTitle, ProductPrice are atomic components that can be reused in other product displays
  • Filter components (CategoryFilter, PriceRangeFilter) are designed to be reusable but currently specific to this feature
  • Pagination component is generic and can be reused in other paginated lists

Component Lifecycle Considerations

  • ProductListingPage mounts with initial URL parameters and fetches data
  • Filters are debounced to prevent excessive API calls during rapid filter changes
  • Virtualization is implemented in ProductGrid to handle large product lists efficiently
  • Component unmounting cleans up any ongoing data fetching or polling operations
// Example of how we'd implement the state machine in React
const useProductListingState = () => {
  const [state, setState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [products, setProducts] = useState<Product[]>([]);
  const [filters, setFilters] = useState<FilterState>({ 
    categories: [], 
    brands: [], 
    priceRange: { min: 0, max: 1000 },
    searchTerm: ''
  });
  const [sort, setSort] = useState<SortOption>('relevance');
  const [currentPage, setCurrentPage] = useState(1);

  const fetchProducts = useCallback(async () => {
    setState('loading');
    try {
      const response = await api.fetchProducts({
        filters,
        sort,
        page: currentPage
      });
      setProducts(response.products);
      setState('success');
    } catch (error) {
      setState('error');
    }
  }, [filters, sort, currentPage]);

  const applyFilters = useCallback((newFilters: FilterState) => {
    setFilters(newFilters);
    setCurrentPage(1);
  }, []);

  return {
    state,
    products,
    filters,
    sort,
    currentPage,
    fetchProducts,
    applyFilters,
    setSort,
    setCurrentPage
  };
};

3. State Management Design

Local state vs. global state decisions

For the Product Listing Page, we'll use a hybrid approach:

  • Local state: Component-specific UI states like active filters, sort options, loading indicators, and pagination state
  • Global state: Product data, user preferences (like favorite products), and filter configurations that span multiple components

State management approach

We'll use Zustand for state management because:

  1. Simplicity: Minimal boilerplate compared to Redux Toolkit
  2. TypeScript support: Excellent type inference and integration
  3. Flexibility: Easy to extract slices for different concerns (products, filters, user preferences)
  4. Performance: No unnecessary re-renders with proper selector usage
  5. Developer experience: Clear, readable state management patterns

State shape and normalization strategy

// Product entity structure
interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: string;
  category: string;
  image: string;
  rating: number;
  inStock: boolean;
  tags: string[];
}

// Filter configuration
interface Filters {
  category: string | null;
  priceRange: [number, number] | null;
  tags: string[];
  searchQuery: string;
}

// User preferences
interface UserPreferences {
  sortOption: 'price-asc' | 'price-desc' | 'rating' | 'name';
  viewMode: 'grid' | 'list';
  favorites: string[]; // product IDs
}

// Main state structure
interface ProductListState {
  products: Product[];
  filteredProducts: Product[];
  filters: Filters;
  preferences: UserPreferences;
  pagination: {
    currentPage: number;
    itemsPerPage: number;
    totalItems: number;
  };
  loading: boolean;
  error: string | null;
}

Server state vs. client state separation

  • Server state: Product data, filter options, and pagination metadata fetched from API
  • Client state: UI state (loading, error), user preferences, and local filter states

Cache invalidation strategy

  1. Product data: Invalidate when filters change or new products are added
  2. Filter options: Invalidate when new categories/tags become available
  3. User preferences: Persist in localStorage and sync with backend on login

Optimistic updates design

For favorite toggling:

  1. Immediately update local state to show product as favorited
  2. Send API request to backend
  3. If successful, keep local state updated
  4. If failed, revert to previous state and show error

Critical-path hook implementation

// src/hooks/useProductList.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: string;
  category: string;
  image: string;
  rating: number;
  inStock: boolean;
  tags: string[];
}

interface Filters {
  category: string | null;
  priceRange: [number, number] | null;
  tags: string[];
  searchQuery: string;
}

interface UserPreferences {
  sortOption: 'price-asc' | 'price-desc' | 'rating' | 'name';
  viewMode: 'grid' | 'list';
  favorites: string[];
}

interface ProductListState {
  products: Product[];
  filteredProducts: Product[];
  filters: Filters;
  preferences: UserPreferences;
  pagination: {
    currentPage: number;
    itemsPerPage: number;
    totalItems: number;
  };
  loading: boolean;
  error: string | null;
  
  // Actions
  setProducts: (products: Product[]) => void;
  applyFilters: (filters: Partial<Filters>) => void;
  setPreferences: (prefs: Partial<UserPreferences>) => void;
  setCurrentPage: (page: number) => void;
  toggleFavorite: (productId: string) => void;
  fetchProducts: () => Promise<void>;
  setError: (error: string | null) => void;
  setLoading: (loading: boolean) => void;
}

const useProductListStore = create<ProductListState>()(
  devtools((set, get) => ({
    // Initial state
    products: [],
    filteredProducts: [],
    filters: {
      category: null,
      priceRange: null,
      tags: [],
      searchQuery: '',
    },
    preferences: {
      sortOption: 'name',
      viewMode: 'grid',
      favorites: [],
    },
    pagination: {
      currentPage: 1,
      itemsPerPage: 20,
      totalItems: 0,
    },
    loading: false,
    error: null,

    // Actions
    setProducts: (products) => {
      set({ products });
      get().applyFilters({});
    },

    applyFilters: (filters) => {
      set((state) => {
        const newFilters = { ...state.filters, ...filters };
        const filtered = state.products.filter(product => {
          // Category filter
          if (newFilters.category && product.category !== newFilters.category) {
            return false;
          }
          
          // Price range filter
          if (newFilters.priceRange) {
            const [min, max] = newFilters.priceRange;
            if (product.price < min || product.price > max) {
              return false;
            }
          }
          
          // Tags filter
          if (newFilters.tags.length > 0) {
            const productTags = new Set(product.tags);
            const filterTags = new Set(newFilters.tags);
            if (!newFilters.tags.every(tag => productTags.has(tag))) {
              return false;
            }
          }
          
          // Search query filter
          if (newFilters.searchQuery) {
            const query = newFilters.searchQuery.toLowerCase();
            const matches = 
              product.name.toLowerCase().includes(query) ||
              product.description.toLowerCase().includes(query) ||
              product.tags.some(tag => tag.toLowerCase().includes(query));
            if (!matches) {
              return false;
            }
          }
          
          return true;
        });
        
        return {
          filters: newFilters,
          filteredProducts: filtered,
        };
      });
    },

    setPreferences: (prefs) => {
      set((state) => ({
        preferences: { ...state.preferences, ...prefs },
      }));
    },

    setCurrentPage: (page) => {
      set((state) => ({
        pagination: { ...state.pagination, currentPage: page },
      }));
    },

    toggleFavorite: (productId) => {
      set((state) => {
        const isFavorite = state.preferences.favorites.includes(productId);
        const newFavorites = isFavorite
          ? state.preferences.favorites.filter(id => id !== productId)
          : [...state.preferences.favorites, productId];
        
        return {
          preferences: {
            ...state.preferences,
            favorites: newFavorites,
          },
        };
      });
    },

    fetchProducts: async () => {
      set({ loading: true, error: null });
      
      try {
        // Simulate API call
        const response = await fetch('/api/products');
        const data = await response.json();
        
        set({
          products: data.products,
          filteredProducts: data.products,
          pagination: {
            ...get().pagination,
            totalItems: data.totalItems,
          },
        });
      } catch (error) {
        set({ error: 'Failed to fetch products' });
      } finally {
        set({ loading: false });
      }
    },

    setError: (error) => set({ error }),
    setLoading: (loading) => set({ loading }),
  }))
);

// Custom hook for simplified usage
export const useProductList = () => {
  const [
    products,
    filteredProducts,
    filters,
    preferences,
    pagination,
    loading,
    error,
    setProducts,
    applyFilters,
    setPreferences,
    setCurrentPage,
    toggleFavorite,
    fetchProducts,
    setError,
    setLoading,
  ] = useProductListStore((state) => [
    state.products,
    state.filteredProducts,
    state.filters,
    state.preferences,
    state.pagination,
    state.loading,
    state.error,
    state.setProducts,
    state.applyFilters,
    state.setPreferences,
    state.setCurrentPage,
    state.toggleFavorite,
    state.fetchProducts,
    state.setError,
    state.setLoading,
  ]);

  return {
    products,
    filteredProducts,
    filters,
    preferences,
    pagination,
    loading,
    error,
    setProducts,
    applyFilters,
    setPreferences,
    setCurrentPage,
    toggleFavorite,
    fetchProducts,
    setError,
    setLoading,
  };
};

The hook returns:

  • State: All product data, filters, preferences, and pagination state
  • Actions: Functions to update state, fetch products, and manage favorites
  • Return shape: Clean, flat object for easy destructuring in components

Key state transitions:

  1. fetchProducts → loading → data loaded → filtered products updated
  2. applyFilters → filtered products updated based on current filters
  3. toggleFavorite → optimistic update to favorites list with fallback on error

Side-effect handler: The fetchProducts action handles API communication and proper loading/error states, ensuring the UI remains responsive during data operations.

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    B --> D[/]
    B --> E[/login]
    B --> F[/register]
    C --> G[/dashboard]
    C --> H[/products]
    H --> I[/products/:id]
    H --> J[/products/:id/reviews]

The product listing page will be accessible at /products and supports nested routes for product details and reviews. Public routes include login/register flows, while dashboard and products are protected.

Code Splitting Strategy

Route-based code splitting with React.lazy:

// routes.tsx
import { lazy } from 'react';

const ProductListPage = lazy(() => import('../pages/ProductListPage'));
const ProductDetailPage = lazy(() => import('../pages/ProductDetailPage'));
const ProductReviewsPage = lazy(() => import('../pages/ProductReviewsPage'));

export const routes = [
  {
    path: '/products',
    element: <ProductListPage />,
    loader: productLoader,
  },
  {
    path: '/products/:id',
    element: <ProductDetailPage />,
    loader: productDetailLoader,
  },
  {
    path: '/products/:id/reviews',
    element: <ProductReviewsPage />,
    loader: productReviewsLoader,
  },
];

Justification: Route-based splitting ensures that the PLP and its related pages load only when needed, reducing initial bundle size. Product detail and reviews pages are loaded on demand.

// auth-guard.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/auth';

export const AuthGuard = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const { isAuthenticated, isLoading } = useAuthStore();

  useEffect(() => {
    if (!isAuthenticated && !isLoading) {
      navigate('/login', { replace: true });
    }
  }, [isAuthenticated, isLoading, navigate]);

  if (isLoading) return <div>Loading...</div>;
  return children;
};

Deep Linking Support and URL Parameter Design

URL structure for PLP:

/products?category=electronics&sort=price-low-to-high&page=1&filters[brand]=apple

URL parameters:

  • category: Filter by product category (e.g., "electronics", "clothing")
  • sort: Sorting options (price-low-to-high, price-high-to-low, rating)
  • page: Pagination page number
  • filters: Nested filter object (brand, priceRange, inStock)

Browser History Management and Scroll Restoration

// scroll-restoration.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export const ScrollRestoration = () => {
  const location = useLocation();

  useEffect(() => {
    window.scrollTo({ top: 0, behavior: 'smooth' });
  }, [location.pathname]);

  return null;
};
// app.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ScrollRestoration } from './components/ScrollRestoration';

function App() {
  return (
    <BrowserRouter>
      <ScrollRestoration />
      <Routes>
        <Route path="/products" element={<ProductListPage />} />
        <Route path="/products/:id" element={<ProductDetailPage />} />
      </Routes>
    </BrowserRouter>
  );
}

The scroll restoration ensures that when users navigate between product listings and details, they are returned to the top of the page. This provides a consistent user experience across navigation flows.

The PLP uses a combination of:

  1. Client-side navigation for internal product browsing
  2. Server-side rendering for SEO and initial load performance (SSR)
  3. Preloading of next page content when user hovers over "next page" buttons
// navigation-utils.ts
export const buildProductUrl = (
  category: string,
  sort: string,
  page: number,
  filters: Record<string, any>
) => {
  const params = new URLSearchParams();
  
  if (category) params.append('category', category);
  if (sort) params.append('sort', sort);
  if (page > 1) params.append('page', page.toString());
  
  Object.entries(filters).forEach(([key, value]) => {
    if (Array.isArray(value)) {
      value.forEach(v => params.append(`filters[${key}]`, v));
    } else {
      params.append(`filters[${key}]`, value.toString());
    }
  });

  return `/products?${params.toString()}`;
};

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using a fetch wrapper pattern with Axios for request handling and response normalization:

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com';

const apiClient: AxiosInstance = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor for auth token injection
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor for error handling and normalization
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle auth errors
      localStorage.removeItem('authToken');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Request/Response Interceptors

Request Interceptor:

  • Injects authentication tokens from localStorage
  • Adds request metadata (timestamp, user agent)
  • Handles retry logic for transient errors

Response Interceptor:

  • Normalizes API responses to consistent format
  • Handles authentication failures (redirect to login)
  • Transforms error responses into user-friendly messages

Error Handling and Retry Strategy

// api/errorHandler.ts
import { AxiosError } from 'axios';

export interface ApiError {
  message: string;
  code?: string;
  status: number;
}

export const handleApiError = (error: AxiosError): ApiError => {
  if (error.response) {
    return {
      message: error.response.data.message || 'An error occurred',
      code: error.response.data.code,
      status: error.response.status,
    };
  }
  
  if (error.request) {
    return {
      message: 'Network error - please check your connection',
      status: 0,
    };
  }
  
  return {
    message: error.message || 'An unknown error occurred',
    status: 500,
  };
};

// Retry strategy with exponential backoff
export const retryWithBackoff = async (
  fn: () => Promise<any>,
  retries = 3,
  delay = 1000
): Promise<any> => {
  try {
    return await fn();
  } catch (error) {
    if (retries > 0 && [500, 502, 503, 504].includes(error?.response?.status)) {
      await new Promise(resolve => setTimeout(resolve, delay));
      return retryWithBackoff(fn, retries - 1, delay * 2);
    }
    throw error;
  }
};

Loading, Error, and Empty States Design

// components/ProductList.tsx
interface ProductListState {
  loading: boolean;
  error: ApiError | null;
  products: Product[];
  hasMore: boolean;
  page: number;
}

const initialProductListState: ProductListState = {
  loading: false,
  error: null,
  products: [],
  hasMore: true,
  page: 1,
};

Data Fetching Strategy

For the Product Listing Page, we'll use Server-Side Rendering (SSR) for initial page load to improve LCP and SEO:

// pages/products/[category].tsx
export async function getServerSideProps(context: any) {
  const { category } = context.params;
  const page = parseInt(context.query.page) || 1;
  
  try {
    const response = await apiClient.get(`/products`, {
      params: { category, page, limit: 20 },
    });
    
    return {
      props: {
        initialProducts: response.data.products,
        totalCount: response.data.totalCount,
        category,
        page,
      },
    };
  } catch (error) {
    return {
      props: {
        initialProducts: [],
        totalCount: 0,
        category,
        page,
        error: handleApiError(error),
      },
    };
  }
}

API Response Transformation Layer

// api/transformers.ts
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: string;
  imageUrls: string[];
  category: string;
  rating: number;
  reviewCount: number;
  inStock: boolean;
}

export const transformProductResponse = (rawProduct: any): Product => ({
  id: rawProduct.id,
  name: rawProduct.name,
  description: rawProduct.description,
  price: rawProduct.price.amount,
  currency: rawProduct.price.currency,
  imageUrls: rawProduct.images?.map((img: any) => img.url) || [],
  category: rawProduct.category,
  rating: rawProduct.rating.average,
  reviewCount: rawProduct.rating.count,
  inStock: rawProduct.inStock,
});

export const transformProductsResponse = (rawResponse: any): {
  products: Product[];
  totalCount: number;
} => ({
  products: rawResponse.items.map(transformProductResponse),
  totalCount: rawResponse.totalCount,
});

Sequence Diagram for Primary Data Flow

sequenceDiagram
    participant U as User
    participant C as Client (Browser)
    participant A as API Gateway
    participant D as Database

    U->>C: Navigate to /products?category=electronics&page=1
    C->>A: GET /products?category=electronics&page=1&limit=20
    A->>D: Query products by category and page
    D-->>A: Return product data + count
    A-->>C: JSON response with products and totalCount
    C->>C: Transform raw API data to domain models
    C-->>U: Render product list UI with loading states

Pagination and Filtering Strategy

The API supports query parameters for filtering and pagination:

// api/queries.ts
export interface ProductFilters {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  rating?: number;
  sortBy?: 'price' | 'rating' | 'name';
  sortOrder?: 'asc' | 'desc';
}

export interface ProductQueryParams {
  page: number;
  limit: number;
  filters: ProductFilters;
}

export const buildProductQueryParams = (params: ProductQueryParams): Record<string, any> => {
  const { page, limit, filters } = params;
  
  const queryParams: Record<string, any> = {
    page,
    limit,
  };

  if (filters.category) queryParams.category = filters.category;
  if (filters.minPrice !== undefined) queryParams.minPrice = filters.minPrice;
  if (filters.maxPrice !== undefined) queryParams.maxPrice = filters.maxPrice;
  if (filters.rating !== undefined) queryParams.minRating = filters.rating;
  if (filters.sortBy) queryParams.sortBy = filters.sortBy;
  if (filters.sortOrder) queryParams.sortOrder = filters.sortOrder;

  return queryParams;
};

Cache Invalidation Strategy

For product listings, we'll implement cache invalidation based on:

  1. Category changes: Invalidate all cached products for that category
  2. Product updates: Invalidate specific product cache entries
  3. Time-based expiration: Cache valid for 5 minutes before refresh
// api/cache.ts
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

export const cacheKey = (endpoint: string, params: any): string => {
  return `${endpoint}?${new URLSearchParams(params).toString()}`;
};

export const getCachedResponse = <T>(key: string): T | null => {
  const cached = localStorage.getItem(key);
  if (!cached) return null;
  
  const { data, timestamp } = JSON.parse(cached);
  if (Date.now() - timestamp > CACHE_TTL) {
    localStorage.removeItem(key);
    return null;
  }
  
  return data;
};

export const setCachedResponse = <T>(key: string, data: T): void => {
  localStorage.setItem(key, JSON.stringify({
    data,
    timestamp: Date.now(),
  }));
};

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for 95% of users
  • Interaction to Next Paint (INP): <200ms for 90% of interactions
  • Cumulative Layout Shift (CLS): <0.1 for 95% of users

Bundle Size Budget per Route

  • Product Listing Page: <200KB (excluding images)
  • Product Detail Page: <150KB (excluding images)
  • Search/Filter UI Components: <50KB total

Critical Rendering Path Optimization

// Critical CSS extraction and inlining for PLP
const criticalCSS = `
  .plp-container { display: flex; }
  .product-card { width: 200px; height: 300px; }
  .loading-skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); }
`;

// Preload critical resources
const preloadCriticalResources = () => {
  const link = document.createElement('link');
  link.rel = 'preload';
  link.as = 'style';
  link.href = '/critical.css';
  document.head.appendChild(link);
};

Image Optimization Strategy

  • Formats: WebP for modern browsers, JPEG fallback for legacy
  • Lazy loading: loading="lazy" attribute on all product images
  • Responsive sizing: sizes and srcset attributes for different viewports
// Image optimization component
const OptimizedProductImage: React.FC<{
  src: string;
  alt: string;
  width?: number;
  height?: number;
}> = ({ src, alt, width = 200, height = 200 }) => {
  return (
    <img
      src={src}
      alt={alt}
      width={width}
      height={height}
      loading="lazy"
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      srcSet={`
        ${src}?w=200 200w,
        ${src}?w=400 400w,
        ${src}?w=600 600w
      `}
    />
  );
};

Font Loading Strategy

/* Critical font subset loading */
@font-face {
  font-family: 'ProductFont';
  src: url('/fonts/critical.woff2') format('woff2');
  font-display: swap;
  font-weight: 400;
}

/* Non-critical fonts loaded after */
@media (max-width: 768px) {
  @font-face {
    font-family: 'ProductFont';
    src: url('/fonts/regular.woff2') format('woff2');
    font-display: swap;
  }
}

Memoization Strategy

// Memoized components to prevent unnecessary re-renders
const ProductCard = React.memo(({ product, onAddToCart }: ProductCardProps) => {
  const [isAdded, setIsAdded] = useState(false);
  
  const handleAddToCart = useCallback(() => {
    onAddToCart(product.id);
    setIsAdded(true);
  }, [product.id, onAddToCart]);

  return (
    <div className="product-card">
      <OptimizedProductImage 
        src={product.image} 
        alt={product.name} 
      />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={handleAddToCart} disabled={isAdded}>
        {isAdded ? 'Added' : 'Add to Cart'}
      </button>
    </div>
  );
});

// Memoized filter component
const FilterPanel = React.memo(({ filters, onFilterChange }: FilterPanelProps) => {
  const [expandedFilters, setExpandedFilters] = useState<Set<string>>(new Set());
  
  const toggleFilterGroup = useCallback((group: string) => {
    setExpandedFilters(prev => {
      const newSet = new Set(prev);
      if (newSet.has(group)) {
        newSet.delete(group);
      } else {
        newSet.add(group);
      }
      return newSet;
    });
  }, []);

  return (
    <div className="filter-panel">
      {filters.map(filter => (
        <FilterGroup 
          key={filter.id}
          filter={filter}
          expanded={expandedFilters.has(filter.id)}
          onToggle={toggleFilterGroup}
          onChange={onFilterChange}
        />
      ))}
    </div>
  );
});

Virtualization for Large Lists

// React Window implementation for large product lists
const VirtualizedProductList: React.FC<{
  products: Product[];
  onAddToCart: (id: string) => void;
}> = ({ products, onAddToCart }) => {
  const itemHeight = 300; // Fixed height for each product card
  const itemCount = products.length;

  const Row: React.FC<{ index: number; style: React.CSSProperties }> = 
    ({ index, style }) => {
      const product = products[index];
      return (
        <div style={style} className="product-card-container">
          <ProductCard 
            product={product} 
            onAddToCart={onAddToCart}
          />
        </div>
      );
    };

  return (
    <List
      height={600}
      itemCount={itemCount}
      itemSize={itemHeight}
      width="100%"
    >
      {Row}
    </List>
  );
};

Prefetching and Preloading Strategy

// Prefetch next page of products when user scrolls near bottom
const useProductPrefetch = (currentPage: number, hasNextPage: boolean) => {
  useEffect(() => {
    if (!hasNextPage) return;
    
    const prefetchNextPage = async () => {
      try {
        // Preload next page of products
        const nextProducts = await fetchProducts(currentPage + 1);
        // Cache in memory or localStorage for faster subsequent loads
        cacheProducts(nextProducts, currentPage + 1);
      } catch (error) {
        console.error('Prefetch failed:', error);
      }
    };

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && hasNextPage) {
          prefetchNextPage();
        }
      },
      { threshold: 0.8 }
    );

    const sentinel = document.getElementById('product-list-sentinel');
    if (sentinel) {
      observer.observe(sentinel);
    }

    return () => observer.disconnect();
  }, [currentPage, hasNextPage]);
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to prevent cascading failures while providing meaningful feedback to users:

graph TD
    A[ProductListingPage] --> B[ProductListContainer]
    A --> C[FilterSidebar]
    A --> D[ProductCard]
    B --> E[ErrorBoundary]
    C --> F[ErrorBoundary]
    D --> G[ErrorBoundary]
    
    E --> H[ProductListFallback]
    F --> I[FilterSidebarFallback]
    G --> J[ProductCardFallback]
  • ProductListContainer: Wraps the entire list with a top-level error boundary to handle critical failures in data loading or rendering
  • FilterSidebar: Has its own error boundary to gracefully handle filter configuration errors
  • ProductCard: Individual cards have error boundaries to prevent one bad product from breaking the entire list

Global Error Handler Design

// src/core/error/GlobalErrorHandler.tsx
import React from 'react';

interface GlobalErrorState {
  hasError: boolean;
  error?: Error;
  errorInfo?: React.ErrorInfo;
}

class GlobalErrorHandler extends React.Component<{}, GlobalErrorState> {
  constructor(props: {}) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): GlobalErrorState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log to monitoring service
    console.error('Global error caught:', error, errorInfo);
    
    // Send to error tracking service (e.g., Sentry)
    this.logErrorToService(error, errorInfo);
  }

  logErrorToService = (error: Error, errorInfo: React.ErrorInfo) => {
    // Implementation for sending to Sentry or similar
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error?.toString()}
            <br />
            {this.state.errorInfo?.componentStack}
          </details>
        </div>
      );
    }

    return this.props.children;
  }
}

export default GlobalErrorHandler;

Offline / Network Failure Handling

// src/core/utils/network.ts
export const isOnline = () => navigator.onLine;

export const useNetworkStatus = () => {
  const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
};

// src/core/error/NetworkErrorBoundary.tsx
import React from 'react';
import { isOnline } from './network';

interface NetworkErrorBoundaryState {
  hasError: boolean;
  isOffline: boolean;
}

class NetworkErrorBoundary extends React.Component<{}, NetworkErrorBoundaryState> {
  constructor(props: {}) {
    super(props);
    this.state = { hasError: false, isOffline: !isOnline() };
  }

  static getDerivedStateFromError(): NetworkErrorBoundaryState {
    return { hasError: true, isOffline: false };
  }

  componentDidMount() {
    window.addEventListener('offline', () => this.setState({ isOffline: true }));
    window.addEventListener('online', () => this.setState({ isOffline: false }));
  }

  componentWillUnmount() {
    window.removeEventListener('offline', () => this.setState({ isOffline: true }));
    window.removeEventListener('online', () => this.setState({ isOffline: false }));
  }

  render() {
    if (this.state.isOffline) {
      return (
        <div className="offline-container">
          <h2>You're offline</h2>
          <p>Please check your internet connection and try again.</p>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }

    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>Something went wrong</h2>
          <p>We couldn't load the product list. Please try again later.</p>
        </div>
      );
    }

    return this.props.children;
  }
}

Graceful Degradation Patterns

// src/features/product-listing/components/GracefulProductList.tsx
import React, { useState, useEffect } from 'react';
import { ProductCard } from './ProductCard';

interface GracefulProductListProps {
  products: Product[];
  loading: boolean;
  error: Error | null;
  retry: () => void;
}

const GracefulProductList: React.FC<GracefulProductListProps> = ({
  products,
  loading,
  error,
  retry
}) => {
  const [fallbackProducts, setFallbackProducts] = useState<Product[]>([]);
  
  useEffect(() => {
    if (error && !loading) {
      // Fallback to cached or sample data when error occurs
      setFallbackProducts([
        { id: 'fallback-1', name: 'Sample Product 1', price: 29.99 },
        { id: 'fallback-2', name: 'Sample Product 2', price: 39.99 },
      ]);
    }
  }, [error, loading]);

  if (loading) {
    return <div className="skeleton-loader">Loading products...</div>;
  }

  if (error) {
    return (
      <div className="error-container">
        <h3>Failed to load products</h3>
        <p>{error.message}</p>
        <button onClick={retry}>Retry</button>
        {fallbackProducts.length > 0 && (
          <div className="fallback-products">
            <h4>Showing fallback products:</h4>
            {fallbackProducts.map(product => (
              <ProductCard key={product.id} product={product} />
            ))}
          </div>
        )}
      </div>
    );
  }

  return (
    <div className="product-list">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
};

export default GracefulProductList;

User Feedback on Errors

// src/core/components/ToastManager.tsx
import React, { useState } from 'react';
import Toast from './Toast';

interface ToastConfig {
  id: string;
  message: string;
  type: 'error' | 'warning' | 'success';
  duration?: number;
}

const ToastManager: React.FC = () => {
  const [toasts, setToasts] = useState<ToastConfig[]>([]);

  const addToast = (config: Omit<ToastConfig, 'id'>) => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { ...config, id }]);
    
    // Auto-remove after duration
    if (config.duration !== 0) {
      setTimeout(() => removeToast(id), config.duration || 5000);
    }
  };

  const removeToast = (id: string) => {
    setToasts(prev => prev.filter(toast => toast.id !== id));
  };

  return (
    <div className="toast-container">
      {toasts.map(toast => (
        <Toast
          key={toast.id}
          message={toast.message}
          type={toast.type}
          onClose={() => removeToast(toast.id)}
        />
      ))}
    </div>
  );
};

export default ToastManager;

// src/core/components/Toast.tsx
interface ToastProps {
  message: string;
  type: 'error' | 'warning' | 'success';
  onClose: () => void;
}

const Toast: React.FC<ToastProps> = ({ message, type, onClose }) => {
  return (
    <div className={`toast toast--${type}`}>
      <span>{message}</span>
      <button onClick={onClose} className="toast__close">✕</button>
    </div>
  );
};

export default Toast;

Error Recovery Strategy

// src/core/hooks/useProductListRecovery.ts
import { useState, useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';

export const useProductListRecovery = () => {
  const [recoveryAttempts, setRecoveryAttempts] = useState(0);
  const queryClient = useQueryClient();

  const handleRecovery = () => {
    // Reset cache and retry
    queryClient.invalidateQueries(['products']);
    
    // Retry with exponential backoff
    const retryDelay = Math.min(1000 * Math.pow(2, recoveryAttempts), 30000);
    setRecoveryAttempts(prev => prev + 1);
    
    setTimeout(() => {
      queryClient.refetchQueries(['products']);
    }, retryDelay);
  };

  return { recoveryAttempts, handleRecovery };
};

8. Testing Strategy

Unit Testing Approach

Component-level testing (70% of test coverage):

  • Test all interactive components with various prop combinations
  • Verify rendering behavior for empty, loading, and error states
  • Validate event handlers and side-effect triggers (e.g., search, filter, sort)
  • Test accessibility attributes and keyboard navigation support

Utility function testing (20% of test coverage):

  • Test data transformation utilities (filtering, sorting, pagination)
  • Validate API response parsing and normalization logic
  • Test helper functions for URL parameter serialization/deserialization

Target coverage: 85% component test coverage, 90% utility test coverage

Integration Testing Strategy

API integration testing (15% of test suite):

  • Test complete data fetching workflows from component to API layer
  • Validate proper error handling and fallback behavior
  • Verify cache invalidation and optimistic update patterns

State management integration:

  • Test custom hooks with real Redux/Zustand store implementations
  • Validate state transitions and side-effect coordination between components

E2E Testing Scope

End-to-end test coverage (5% of test suite):

  • Core user flows: search → filter → sort → view product details
  • Multi-step filtering and sorting combinations
  • Responsive behavior testing across device breakpoints

Testing tool: Playwright (multi-browser support, reliable DOM handling)

  • Test 3 primary user journeys: search, filter, and sort
  • Validate cross-browser compatibility (Chrome, Firefox, Safari)
  • Include mobile viewport testing with touch simulation

Visual Regression Testing

Screenshot-based validation:

  • Test all major UI states (loading, empty, error, populated)
  • Validate responsive layouts across breakpoints (mobile, tablet, desktop)
  • Monitor for unexpected design changes in filter sidebar and product grid

Accessibility Testing Automation

Automated axe-core integration:

  • Run accessibility checks on all interactive components
  • Validate proper ARIA attribute usage and semantic HTML structure
  • Test keyboard navigation flow through all filterable elements

Test Data Strategy

Test data fixtures:

  • Create realistic mock product datasets with various attributes (price, rating, category)
  • Include edge cases: out-of-stock items, zero-rated products, missing images
  • Use deterministic fixtures for consistent test results and debugging

Test data isolation:

  • Each test runs with isolated mock API responses to prevent flaky tests
  • Use factory patterns for generating diverse test scenarios (e.g., 100 products with 5 categories)
  • Implement data cleanup between test runs to maintain test isolation

9. Accessibility (A11y)

WCAG 2.1 AA Compliance Checklist

The Product Listing Page must meet WCAG 2.1 AA standards with the following key compliance areas:

  • Perceivable: All content must be presented in ways that users can perceive (text alternatives, sufficient color contrast, adaptable content)
  • Operable: Interface must be operable through keyboard navigation and time limits
  • Understandable: Content and interface must be understandable to users
  • Robust: Content must be robust enough to be interpreted reliably by assistive technologies

Keyboard Navigation Support

All interactive elements must be fully navigable using only keyboard:

  • Products cards can be focused and activated via Enter/Space
  • Filter sidebar supports full keyboard navigation (Tab, Shift+Tab, Arrow keys)
  • Pagination controls are keyboard accessible
  • Search input supports keyboard search submission (Enter key)

Focus Management Strategy

Focus Trapping: Filter sidebar modals will implement proper focus trapping to keep keyboard users within the modal until they explicitly close it.

Focus Restoration: When modals close, focus returns to the triggering element (e.g., "Filters" button).

Focus Indicators: Clear visual focus indicators for all interactive elements with sufficient contrast (minimum 3:1 ratio).

ARIA Roles and Attributes Plan

// ProductCard component ARIA implementation
interface ProductCardProps {
  product: Product;
  index: number;
  isFeatured?: boolean;
}

// ARIA roles and attributes for accessible product cards
const ProductCard: React.FC<ProductCardProps> = ({ product, index, isFeatured }) => {
  return (
    <div 
      role="article"
      aria-label={`Product: ${product.name}`}
      tabIndex={0}
      className="product-card"
      data-testid={`product-card-${product.id}`}
    >
      <img 
        src={product.image} 
        alt={product.name}
        role="img"
        aria-describedby={`product-description-${product.id}`}
      />
      <div 
        role="heading" 
        aria-level={3}
        className="product-title"
      >
        {product.name}
      </div>
      <p id={`product-description-${product.id}`} className="product-description">
        {product.description}
      </p>
      <div 
        role="status"
        aria-live="polite"
        className="product-price"
      >
        {formatCurrency(product.price)}
      </div>
    </div>
  );
};

Screen Reader Compatibility

All content must be readable by screen readers:

  • Semantic HTML structure (h1, h2, h3, section, article)
  • Proper heading hierarchy for content organization
  • ARIA labels for dynamic content updates (e.g., "Showing 12 of 48 products")
  • Descriptive alt text for all images
  • Live regions for status updates (e.g., "Search results updated")

Additional A11y Considerations

  • Color Contrast: All text must meet 4.5:1 contrast ratio against background
  • Zoom Support: Content must remain usable at 200% zoom level
  • Responsive Layout: Mobile-first design with proper touch targets (minimum 44px)
  • Skip Links: "Skip to main content" link for keyboard users
  • Form Labels: All form controls must have associated labels
  • Error States: Clear error messages with proper ARIA attributes (aria-invalid, aria-describedby)
  • Loading States: Progress indicators for content loading with ARIA live regions

Keyboard Navigation Flow

  1. Main Content: Tab through products in grid view
  2. Filter Panel: Keyboard navigation through filter options
  3. Pagination: Navigation through page numbers using arrow keys
  4. Search: Enter key to submit search queries
  5. Sorting: Keyboard access to sort dropdown options

Focus Order Validation

The focus order will be validated using automated tools and manual testing to ensure logical flow:

  1. Header navigation (skip link → logo → nav links)
  2. Search bar
  3. Filter button
  4. Product grid items (left-to-right, top-to-bottom)
  5. Pagination controls
  6. Footer navigation

Testing Strategy for A11y

  • Automated Testing: axe-core integration in unit tests
  • Manual Testing: Keyboard-only navigation testing
  • Screen Reader Testing: NVDA, JAWS, and VoiceOver compatibility testing
  • Color Contrast Testing: Using WAVE or similar tools
  • Tab Order Validation: Through browser developer tools and accessibility inspectors

Accessibility Testing Automation

// Example of accessibility testing in Jest
describe('ProductListingPage', () => {
  it('should have proper focus order and keyboard navigation', () => {
    const { container } = render(<ProductListingPage />);
    
    // Verify skip link exists and is first in tab order
    expect(container.querySelector('[href="#main-content"]')).toBeInTheDocument();
    
    // Verify all products are keyboard focusable
    const productCards = container.querySelectorAll('[role="article"]');
    expect(productCards.length).toBeGreaterThan(0);
    
    // Verify proper ARIA attributes
    expect(productCards[0]).toHaveAttribute('aria-label');
  });
});

10. Security Considerations

XSS Prevention Strategy

The PLP must implement robust XSS protection to prevent malicious script execution from user-generated content or API responses:

// Secure HTML rendering utility
const sanitizeHTML = (rawHTML: string): string => {
  const div = document.createElement('div');
  div.innerHTML = rawHTML;
  return div.textContent || div.innerText || '';
};

// Component-level XSS protection
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
  const safeDescription = sanitizeHTML(product.description);
  
  return (
    <div className="product-card">
      <h3 dangerouslySetInnerHTML={{ __html: product.name }} />
      <p dangerouslySetInnerHTML={{ __html: safeDescription }} />
    </div>
  );
};

CSRF Protection

All API requests must include CSRF tokens to prevent cross-site request forgery:

// CSRF token interceptor pattern
const apiClient = axios.create({
  baseURL: '/api',
  withCredentials: true,
});

apiClient.interceptors.request.use((config) => {
  const csrfToken = getCSRFTokenFromCookie();
  if (csrfToken) {
    config.headers['X-CSRF-Token'] = csrfToken;
  }
  return config;
});

Content Security Policy (CSP) Headers

The server must enforce CSP headers to restrict script execution:

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' https://trusted-cdn.com; 
  style-src 'self' 'unsafe-inline'; 
  img-src 'self' data: https:; 
  font-src 'self' https:;

Sensitive Data Handling in Frontend State

Frontend state management must never store sensitive user data:

// Secure state design - avoid storing sensitive fields
interface ProductState {
  // ❌ DO NOT store sensitive data like user emails, payment info
  products: Product[];
  filters: FilterState;
  pagination: PaginationState;
  // ✅ Safe to store
}

// Product model without sensitive fields
interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  image: string;
  rating: number;
}

Input Sanitization Approach

All user inputs and URL parameters must be sanitized before processing:

// Input sanitization utilities
const sanitizeFilterParams = (params: Record<string, string>): Record<string, string> => {
  const sanitized: Record<string, string> = {};
  
  Object.entries(params).forEach(([key, value]) => {
    // Remove potentially dangerous characters
    const cleanValue = value.replace(/[<>'"&]/g, '');
    
    // Validate parameter names
    if (ALLOWED_FILTER_PARAMS.includes(key)) {
      sanitized[key] = cleanValue;
    }
  });
  
  return sanitized;
};

// URL parameter validation
const validatePageNumber = (pageNum: string): number => {
  const num = parseInt(pageNum, 10);
  if (isNaN(num) || num < 1) return 1;
  return Math.min(num, MAX_PAGE_LIMIT);
};

Security Headers Implementation

// Security headers configuration (server-side)
const securityHeaders = {
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'Referrer-Policy': 'strict-origin-when-cross-origin'
};

Session Management

Session tokens must be handled securely:

// Secure session handling
const handleAuthChange = (newToken: string) => {
  // Store in HTTP-only cookie for security
  document.cookie = `authToken=${newToken}; Secure; HttpOnly; SameSite=Strict`;
  
  // Clear any potentially unsafe local storage
  localStorage.removeItem('unsafe_auth_data');
};

Data Validation

All API responses must be validated before use:

// Type-safe response validation
const validateProductResponse = (data: any): Product[] => {
  if (!Array.isArray(data)) {
    throw new Error('Invalid product data format');
  }
  
  return data.map(item => {
    if (!item.id || !item.name) {
      throw new Error('Missing required product fields');
    }
    
    return {
      id: item.id,
      name: item.name,
      description: item.description || '',
      price: item.price || 0,
      image: item.image || '',
      rating: item.rating || 0
    };
  });
};

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Translation keys will follow a hierarchical structure using dot notation for maintainability and organization:

products.listing.title
products.listing.filters.priceRange
products.listing.filters.category
products.listing.filters.rating
products.listing.noResults
products.listing.sortBy
products.listing.sortOptions.priceLowToHigh
products.listing.sortOptions.ratingHighToLow
products.listing.itemsPerPage
products.listing.pagination.next
products.listing.pagination.previous

Keys will use kebab-case for readability and avoid special characters. Nested keys will group related content under a common prefix.

RTL Support Considerations

RTL (Right-to-Left) support will be implemented through CSS classes and dynamic class application:

// i18n context hook
const useI18n = () => {
  const [locale, setLocale] = useState('en-US');
  const isRTL = useMemo(() => {
    return ['ar-SA', 'he-IL', 'fa-IR'].includes(locale);
  }, [locale]);

  return {
    locale,
    isRTL,
    setLocale
  };
};

CSS will use direction: rtl for RTL languages and appropriate text alignment properties. Component layouts will conditionally apply RTL-specific styling using CSS modules or styled-components.

Date, Number, Currency Formatting Strategy

All date, number, and currency formatting will leverage the browser's built-in Intl API:

// i18n utils
const formatCurrency = (amount: number, locale: string, currency: string) => {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(amount);
};

const formatDate = (date: Date, locale: string) => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'short',
    day: 'numeric'
  }).format(date);
};

const formatNumber = (num: number, locale: string) => {
  return new Intl.NumberFormat(locale).format(num);
};

Locale Detection and Switching Strategy

Automatic locale detection will be implemented using:

  1. Browser language preference: navigator.language and navigator.languages
  2. User preference persistence: Store selected locale in localStorage
  3. URL parameter support: /en/products or /ar/products for explicit language selection
// i18n service
class I18nService {
  private readonly DEFAULT_LOCALE = 'en-US';
  private readonly SUPPORTED_LOCALES = ['en-US', 'ar-SA', 'fr-FR', 'de-DE'];
  
  detectLocale(): string {
    const savedLocale = localStorage.getItem('locale');
    if (savedLocale && this.SUPPORTED_LOCALES.includes(savedLocale)) {
      return savedLocale;
    }
    
    const browserLocale = navigator.language || 'en-US';
    const matchedLocale = this.SUPPORTED_LOCALES.find(locale => 
      locale.startsWith(browserLocale.split('-')[0])
    );
    
    return matchedLocale || this.DEFAULT_LOCALE;
  }
  
  switchLocale(locale: string) {
    if (this.SUPPORTED_LOCALES.includes(locale)) {
      localStorage.setItem('locale', locale);
      document.documentElement.lang = locale;
      // Trigger re-render or refresh translations
    }
  }
}

The system will support both automatic detection and explicit user selection, with the URL parameter taking precedence over other methods for deep linking purposes.

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The PLP should capture key performance indicators to understand real user experience:

  • Largest Contentful Paint (LCP): Target <2.5s for 75% of users, measured from page load start to largest image/content render
  • First Input Delay (FID): Target <100ms for 90% of users, measuring time from first user interaction to browser response
  • Cumulative Layout Shift (CLS): Target <0.1 for 90% of users, measuring visual stability during loading
  • First Contentful Paint (FCP): Target <1.5s for 75% of users, measuring time to first content render
  • Time to Interactive (TTI): Target <5s for 90% of users, measuring when page becomes fully interactive

Error Tracking Integration

Integrate Sentry or similar error tracking service with:

  • Global error boundaries to capture unhandled promise rejections and component errors
  • Custom error logging for failed API requests, malformed data responses, and client-side validation failures
  • Error grouping by error type, URL path, and user session to identify root causes
  • Error breadcrumbs for API calls, navigation events, and user interactions leading up to failures

Custom Events and User Journey Tracking

Track key user behaviors:

  • Product view events: When users click on a product card to view details
  • Filter application events: When filters are applied (category, price range, rating)
  • Sort order changes: When users change sorting options (price, popularity, rating)
  • Load more events: When infinite scroll triggers additional product fetches
  • Search term usage: For search functionality integration (if applicable)
  • Add to cart events: When users add items to their cart from the listing page

Performance Monitoring and Regression Detection

Implement performance regression detection:

  • Baseline measurement: Establish performance baselines for each route and device type
  • Automated alerting: Set up alerts when performance metrics exceed thresholds by 20% over 30-day rolling average
  • A/B testing capability: Track performance impact of UI/UX changes through A/B experiments
  • Device and browser-specific metrics: Monitor performance across mobile, desktop, and various browser versions

Integration Points

  • Application Performance Monitoring (APM): Integrate with tools like New Relic or Datadog for full-stack monitoring
  • Browser Performance API: Use performance.getEntries() to capture detailed timing data for page load and resource loading
  • Custom metrics dashboard: Build dashboards in Grafana or similar tools to visualize performance trends over time
  • User session replay: Integrate with tools like LogRocket or FullStory to capture user behavior for debugging complex issues

Data Collection Strategy

  • Sampling rate: Collect 100% of critical performance data, 5% of non-critical user interactions
  • Privacy compliance: Ensure all tracking adheres to GDPR, CCPA, and other relevant privacy regulations
  • Data retention: Retain performance data for 90 days for trend analysis, error logs for 30 days
  • Anonymization: All user data should be anonymized and aggregated for analysis to protect user privacy

Alerting Strategy

  • High severity: >50% increase in LCP or FID, >20% increase in error rate
  • Medium severity: >25% increase in CLS, >10% increase in page load time
  • Low severity: Performance degradation below 5% threshold, minor layout shifts

The monitoring strategy should provide actionable insights to optimize the PLP experience while maintaining user privacy and system reliability.

13. File & Folder Structure

Proposed Directory Structure

/src
├── components/
│   ├── product/
│   │   ├── ProductCard.tsx
│   │   ├── ProductList.tsx
│   │   └── ProductFilters.tsx
│   ├── layout/
│   │   ├── Header.tsx
│   │   └── Footer.tsx
│   └── shared/
│       ├── Button.tsx
│       ├── Spinner.tsx
│       └── Pagination.tsx
├── features/
│   └── productListing/
│       ├── index.ts
│       ├── ProductListingPage.tsx
│       ├── hooks/
│       │   └── useProductList.ts
│       ├── types/
│       │   └── product.types.ts
│       └── utils/
│           └── product.utils.ts
├── hooks/
│   ├── useDebounce.ts
│   └── useIntersectionObserver.ts
├── services/
│   ├── api/
│   │   └── productService.ts
│   └── cache/
│       └── productCache.ts
├── store/
│   ├── product/
│   │   ├── productSlice.ts
│   │   └── selectors.ts
│   └── index.ts
├── types/
│   ├── common.types.ts
│   └── product.types.ts
├── utils/
│   ├── format.ts
│   └── validation.ts
└── routes/
    └── productListing.routes.tsx

Module Boundaries and Import Rules

  • Feature isolation: Product listing feature is self-contained with its own directory structure
  • No cross-feature imports: Components in productListing feature cannot directly import from other features
  • Shared components: Reusable UI elements live in components/shared/ and can be imported anywhere
  • Barrel exports: Feature directories use barrel exports to simplify imports:
    // features/productListing/index.ts
    export { default as ProductListingPage } from './ProductListingPage';
    export { useProductList } from './hooks/useProductList';

Shared vs Feature-Specific Code

Shared code (components, hooks, utils):

  • Reusable across multiple features
  • Located in /src/components/ and /src/hooks/
  • Follow consistent design system patterns

Feature-specific code:

  • Product listing page logic and components
  • Located in /src/features/productListing/
  • Contains feature-specific business logic and data flow

This structure enables clear separation of concerns, supports team parallel development, and makes the codebase maintainable as features scale.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

React (18.2+) - Core framework for building UI components with concurrent rendering and automatic batching.

TypeScript (5.0+) - Static type checking for improved developer experience, better IDE support, and reduced runtime errors.

React Router DOM (6.15+) - Declarative routing with nested routes, lazy loading, and client-side navigation capabilities.

React Query (5.0+) - Production-ready data fetching and state management with built-in caching, background updates, and automatic refetching.

Zustand (4.3+) - Lightweight state management solution for local component state and cross-component communication without boilerplate.

Tailwind CSS (3.3+) - Utility-first CSS framework enabling rapid UI development with consistent design system and theme support.

Framer Motion (10.16+) - Production-ready animation library for smooth, performant UI transitions and micro-interactions.

React Hook Form (7.48+) - Performant, flexible form library with built-in validation and error handling.

date-fns (2.30+) - Modern date utility library with modular design, immutability, and tree-shaking support.

lodash-es (4.17+) - Utility library for common functional programming patterns with ES module support.

Alternatives Considered and Rejected

Redux Toolkit - While powerful, introduces unnecessary complexity for this feature's state needs. Zustand provides simpler API with better developer experience.

Styled Components - Overkill for this feature's design system needs. Tailwind provides better performance and easier maintenance.

Emotion - Similar to Styled Components, adds complexity without significant benefit for this use case.

Axios - Fetch API with React Query provides sufficient functionality with better performance characteristics and smaller bundle size.

Jotai - More complex than needed for this feature's state requirements and has higher learning curve.

Licensing and Bundle Size Impact Assessment

React Query (~20KB gzipped) - MIT license, provides excellent caching and server state management with minimal overhead.

Zustand (~3KB gzipped) - MIT license, lightweight alternative to Redux with minimal bundle impact.

Tailwind CSS (~10KB gzipped) - MIT license, provides extensive utility classes with minimal runtime overhead.

Framer Motion (~15KB gzipped) - MIT license, provides performant animations with minimal impact on overall bundle size.

date-fns (~2KB gzipped) - MIT license, modular design allows for tree-shaking to reduce bundle size.

lodash-es (~5KB gzipped) - MIT license, provides utility functions with tree-shaking support to minimize impact.

Bundle Size Budget Allocation

  • Core React + DOM: ~150KB
  • State Management (Zustand + React Query): ~25KB
  • UI Framework (Tailwind CSS): ~10KB
  • Animation Library: ~15KB
  • Utility Libraries (date-fns, lodash): ~7KB
  • Form Handling: ~10KB
  • Total Estimated Bundle Size: ~227KB (under 250KB target)

Performance Considerations

All libraries are selected with tree-shaking support to minimize bundle size. React Query and Zustand provide zero-runtime overhead for their features, while Tailwind's JIT compilation ensures only used utilities are included in the final build.

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Filtering vs. Search: Should we prioritize search functionality over filtering, or support both simultaneously? This affects the UI complexity and API design.
  • Product data model: How granular should product data be in the initial load vs. on-demand fetches? Should we pre-fetch all variants or lazy-load them?
  • Pagination strategy: Should we implement infinite scroll or traditional pagination? This impacts performance and SEO considerations.

Known trade-offs made in this design

  • API vs. Client-side filtering: We're leaning toward client-side filtering for better UX responsiveness, but this trades memory usage and initial load time for reduced API calls. For large datasets, we may need to implement server-side filtering.
  • Component reusability vs. performance: We're using a generic ProductCard component that can be reused across different contexts, but this introduces slight performance overhead from additional prop handling and conditional rendering.
  • State management granularity: We're using a single global state for product listings, which simplifies the API but increases the risk of unnecessary re-renders when unrelated state changes occur.

Risks and mitigations

  • Performance degradation with large datasets: If the product catalog grows significantly, client-side filtering and rendering may cause jank. Mitigation includes implementing virtualization for large lists and lazy loading of non-critical components.
  • API rate limiting: High-frequency filtering operations could hit API rate limits. Mitigation includes debounced search and caching strategies.
  • SEO impact of client-side rendering: The PLP is a key SEO page. Mitigation includes server-side rendering (SSR) for initial load and proper meta tags.

Technical debt acknowledged

  • Hardcoded filter options: Filter options are currently hardcoded in the component, which makes them difficult to maintain and update without code changes. This should be abstracted into a configuration system.
  • Limited ARIA support: The current implementation provides basic ARIA attributes but doesn't fully implement role hierarchy for screen readers in complex filtering scenarios.
  • No progressive enhancement: The design assumes modern browser capabilities and doesn't provide fallbacks for older browsers or users with JavaScript disabled.
  • Tight coupling between components: The ProductGrid and FilterPanel components are tightly coupled through state sharing, which could make future component isolation more difficult.
  • Inconsistent error handling: Different parts of the component tree handle errors differently (some show toasts, others inline), which creates inconsistency in user experience.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend LLD

    Checkout Flow Design System

    Comprehensive low-level design for e-commerce checkout flow with payment processing, order management, and user experience optimization

  • Frontend LLD

    Notification Panel Design System

    Comprehensive low-level design for a scalable, accessible notification panel component with real-time updates and user preferences.

  • Frontend LLD

    Comprehensive Sidebar Navigation Menu Design Patterns

    Detailed low-level design for responsive sidebar navigation with collapsible sections, active state management, and accessibility features

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.