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
- What's the target browser support matrix? (e.g., IE11, mobile Safari, Chrome 80+)
- Do we need real-time product updates or is static data sufficient?
- Should the PLP support offline browsing with cached results?
- What's the expected accessibility level? (e.g., WCAG 2.1 AA compliance required?)
- Is i18n support needed for international markets or just English?
- Are there existing design system components we should leverage?
- 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 flowProductFilters- manages filter state and applies filters to product dataProductGrid- renders the grid of products with virtualization support
Dumb Components (Presentational Components):
ProductCard- displays individual product with image, title, price, ratingCategoryFilter,PriceRangeFilter,BrandFilter- filter UI controlsSortOptions- dropdown or button group for sorting optionsPageControls- pagination navigation controlsProductImage,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
ProductCardis highly reusable across different product contextsProductImage,ProductTitle,ProductPriceare 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
ProductListingPagemounts with initial URL parameters and fetches data- Filters are debounced to prevent excessive API calls during rapid filter changes
- Virtualization is implemented in
ProductGridto 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:
- Simplicity: Minimal boilerplate compared to Redux Toolkit
- TypeScript support: Excellent type inference and integration
- Flexibility: Easy to extract slices for different concerns (products, filters, user preferences)
- Performance: No unnecessary re-renders with proper selector usage
- 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
- Product data: Invalidate when filters change or new products are added
- Filter options: Invalidate when new categories/tags become available
- User preferences: Persist in localStorage and sync with backend on login
Optimistic updates design
For favorite toggling:
- Immediately update local state to show product as favorited
- Send API request to backend
- If successful, keep local state updated
- 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:
fetchProducts→ loading → data loaded → filtered products updatedapplyFilters→ filtered products updated based on current filterstoggleFavorite→ 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.
Navigation Guards and Authentication Flows
// 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 numberfilters: 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.
Navigation Strategy for PLP
The PLP uses a combination of:
- Client-side navigation for internal product browsing
- Server-side rendering for SEO and initial load performance (SSR)
- 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:
- Category changes: Invalidate all cached products for that category
- Product updates: Invalidate specific product cache entries
- 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:
sizesandsrcsetattributes 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
- Main Content: Tab through products in grid view
- Filter Panel: Keyboard navigation through filter options
- Pagination: Navigation through page numbers using arrow keys
- Search: Enter key to submit search queries
- 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:
- Header navigation (skip link → logo → nav links)
- Search bar
- Filter button
- Product grid items (left-to-right, top-to-bottom)
- Pagination controls
- 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:
- Browser language preference:
navigator.languageandnavigator.languages - User preference persistence: Store selected locale in localStorage
- URL parameter support:
/en/productsor/ar/productsfor 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
productListingfeature 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
ProductCardcomponent 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
ProductGridandFilterPanelcomponents 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.