Exercise 3: Dynamic List Rendering
Objective
Master list rendering, keys, filtering, sorting, and CRUD operations.
Task
Build a product management dashboard with full CRUD functionality.
Requirements
1. Product Interface
interface Product {
id: string;
name: string;
category: 'electronics' | 'clothing' | 'food' | 'books';
price: number;
stock: number;
rating: number;
createdAt: Date;
}
2. Features
Display
- Grid view of product cards
- Show all product details
- Display stock status (In Stock / Low Stock / Out of Stock)
- Show rating as stars
- Format price with currency
Search & Filter
- Search by product name (real-time)
- Filter by category
- Filter by stock status
- Filter by price range (slider or inputs)
- Show number of results
Sort
- Sort by name (A-Z, Z-A)
- Sort by price (low to high, high to low)
- Sort by rating
- Sort by stock level
- Sort by newest/oldest
CRUD Operations
- Create: Add new product with form
- Read: Display all products
- Update: Edit product details inline or in modal
- Delete: Remove product with confirmation
3. UI Components
// Main component
function ProductDashboard() {
// State management
// Filter/sort logic
// CRUD handlers
}
// Product card
function ProductCard({ product, onEdit, onDelete }) {
// Display product
// Action buttons
}
// Add/Edit form
function ProductForm({ product, onSave, onCancel }) {
// Form fields
// Validation
// Submit handler
}
// Filters panel
function FiltersPanel({ filters, onFilterChange }) {
// Search input
// Category filter
// Price range
// Stock filter
}
// Sort dropdown
function SortDropdown({ sortBy, onSortChange }) {
// Sort options
}
Sample Data
const sampleProducts: Product[] = [
{
id: '1',
name: 'Wireless Headphones',
category: 'electronics',
price: 89.99,
stock: 45,
rating: 4.5,
createdAt: new Date('2024-01-15'),
},
{
id: '2',
name: 'Cotton T-Shirt',
category: 'clothing',
price: 24.99,
stock: 120,
rating: 4.2,
createdAt: new Date('2024-02-01'),
},
// Add more sample products...
];
Implementation Guidelines
State Management
const [products, setProducts] = useState<Product[]>(sampleProducts);
const [searchTerm, setSearchTerm] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string>('all');
const [sortBy, setSortBy] = useState<string>('name-asc');
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
Filter Logic
const filteredProducts = useMemo(() => {
return products
.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
.filter(p => categoryFilter === 'all' || p.category === categoryFilter)
.filter(p => p.price >= priceRange.min && p.price <= priceRange.max);
}, [products, searchTerm, categoryFilter, priceRange]);
Sort Logic
const sortedProducts = useMemo(() => {
const sorted = [...filteredProducts];
switch (sortBy) {
case 'name-asc':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case 'name-desc':
return sorted.sort((a, b) => b.name.localeCompare(a.name));
case 'price-asc':
return sorted.sort((a, b) => a.price - b.price);
case 'price-desc':
return sorted.sort((a, b) => b.price - a.price);
// Add more sort cases...
default:
return sorted;
}
}, [filteredProducts, sortBy]);
CRUD Handlers
const addProduct = (product: Omit<Product, 'id' | 'createdAt'>) => {
const newProduct: Product = {
...product,
id: crypto.randomUUID(),
createdAt: new Date(),
};
setProducts(prev => [...prev, newProduct]);
};
const updateProduct = (id: string, updates: Partial<Product>) => {
setProducts(prev => prev.map(p => (p.id === id ? { ...p, ...updates } : p)));
};
const deleteProduct = (id: string) => {
if (confirm('Are you sure you want to delete this product?')) {
setProducts(prev => prev.filter(p => p.id !== id));
}
};
UI Layout
┌─────────────────────────────────────────┐
│ Product Dashboard │
├─────────────────────────────────────────┤
│ Search: [____________] [+ Add Product]│
├───────────┬─────────────────────────────┤
│ Filters │ Sort: [Dropdown ▼] │
│ │ Showing 12 of 50 products │
│ Category ├─────────────────────────────┤
│ □ All │ ┌────┐ ┌────┐ ┌────┐ │
│ □ Electr. │ │ P1 │ │ P2 │ │ P3 │ │
│ □ Cloth. │ └────┘ └────┘ └────┘ │
│ □ Food │ ┌────┐ ┌────┐ ┌────┐ │
│ □ Books │ │ P4 │ │ P5 │ │ P6 │ │
│ │ └────┘ └────┘ └────┘ │
│ Price │ │
│ [$-$$$] │ [Load More] │
│ │ │
│ Stock │ │
│ ○ All │ │
│ ○ In Stock│ │
│ ○ Low │ │
└───────────┴─────────────────────────────┘
Bonus Challenges
- Add pagination or infinite scroll
- Implement bulk delete
- Add export to CSV functionality
- Persist data to localStorage
- Add product images with upload
- Implement undo/redo for delete
- Add keyboard shortcuts (Ctrl+K for search, etc.)
- Create a table view toggle
- Add favorites/wishlist feature
- Implement drag-and-drop reordering
Testing Checklist
- Products render correctly with proper keys
- Search filters in real-time
- Category filter works
- Price range filter works
- All sort options work correctly
- Add product form validates correctly
- Edit product updates correctly
- Delete confirms and removes product
- No products found message shows when filtered to 0
- Stock status displays correctly
- Rating stars render properly
- Product count updates correctly
Time Estimate
4-5 hours
Hints
- Use
useMemofor filtered/sorted lists to avoid re-computation - Generate stable IDs with
crypto.randomUUID() - Use proper TypeScript types throughout
- Break down into small, reusable components
- Consider using CSS Grid for the product grid layout