# Lists & Dynamic Rendering

When building the POS system's inventory interface, I wasn't displaying one or two products—I was rendering hundreds of menu items, retail products, and variations across multiple restaurant locations and retail stores. Hardcoding each `<ProductCard />` would have been impossible. I needed to dynamically generate UI from data arrays.

This is where React's list rendering becomes critical. Whether you're displaying 10 products or 10,000, the pattern is the same.

## The Problem: Too Much Repetition

Imagine rendering just 5 products manually:

```typescript
function ProductCatalog() {
  return (
    <div>
      <ProductCard name="Mohinga" price={3500} stock={24} category="Breakfast" />
      <ProductCard name="Shan Noodles" price={4000} stock={15} category="Lunch" />
      <ProductCard name="Tea Leaf Salad" price={3000} stock={0} category="Appetizer" />
      <ProductCard name="Samosa" price={1500} stock={45} category="Snack" />
      <ProductCard name="Mandalay Noodles" price={3800} stock={12} category="Lunch" />
    </div>
  );
}
```

Now imagine 500 products. Impossible to maintain. What if prices change? What if you need to add a product?

## The Solution: Map Over Arrays

JavaScript's `.map()` method transforms arrays. In React, we use it to transform data arrays into component arrays:

```typescript
// ProductCatalog.tsx
import React from 'react';
import ProductCard from './ProductCard';

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  category: string;
}

function ProductCatalog() {
  const products: Product[] = [
    { id: '1', name: 'Mohinga', price: 3500, stock: 24, category: 'Breakfast' },
    { id: '2', name: 'Shan Noodles', price: 4000, stock: 15, category: 'Lunch' },
    { id: '3', name: 'Tea Leaf Salad', price: 3000, stock: 0, category: 'Appetizer' },
    { id: '4', name: 'Samosa', price: 1500, stock: 45, category: 'Snack' },
    { id: '5', name: 'Mandalay Noodles', price: 3800, stock: 12, category: 'Lunch' },
  ];
  
  return (
    <div className="product-catalog">
      {products.map(product => (
        <ProductCard 
          key={product.id}
          name={product.name}
          price={product.price}
          stock={product.stock}
          category={product.category}
        />
      ))}
    </div>
  );
}

export default ProductCatalog;
```

**What's happening:**

1. `products.map()` iterates over each product
2. For each product, we return a `<ProductCard />` component
3. We spread the product data as props
4. `key={product.id}` helps React track which items changed (more on this soon)

## Understanding Keys

Notice the `key` prop? **It's required** when rendering lists. Let's understand why.

### Why Keys Matter

When your list changes (items added, removed, or reordered), React needs to figure out **what changed**. Keys help React identify which items changed.

**Without keys:**

```typescript
{products.map(product => (
  <ProductCard {...product} /> // ⚠️ Warning: Each child should have a unique "key"
))}
```

**With keys:**

```typescript
{products.map(product => (
  <ProductCard key={product.id} {...product} /> // ✅ Good
))}
```

### Key Rules

**✅ Use unique, stable IDs**

```typescript
key={product.id} // ✅ Perfect - unique database ID
```

**❌ Don't use array index (usually)**

```typescript
{products.map((product, index) => (
  <ProductCard key={index} {...product} /> // ⚠️ Can cause bugs
))}
```

**Why is index bad?** If items reorder, the index changes, but React thinks it's the same item. This causes bugs with state and performance issues.

**When index is okay:** If your list never reorders, filters, or changes, index is fine. But 99% of the time, use IDs.

### Real Example from POS System

In my POS, products come from the Inventory Service with UUIDs:

```typescript
const products = [
  { id: 'uuid-1a2b3c', name: 'Mohinga', price: 3500, stock: 24, category: 'Breakfast' },
  { id: 'uuid-4d5e6f', name: 'Shan Noodles', price: 4000, stock: 15, category: 'Lunch' },
  // ... hundreds more
];

return (
  <div className="product-grid">
    {products.map(product => (
      <ProductCard 
        key={product.id} // ← UUID from database
        product={product}
      />
    ))}
  </div>
);
```

## Passing Whole Objects as Props

Instead of spreading individual properties, pass the entire object:

```typescript
// Before: Individual props
<ProductCard 
  key={product.id}
  name={product.name}
  price={product.price}
  stock={product.stock}
  category={product.category}
/>

// After: Whole object (cleaner)
<ProductCard 
  key={product.id}
  product={product}
/>
```

The ProductCard component receives it:

```typescript
interface ProductCardProps {
  product: Product;
}

function ProductCard({ product }: ProductCardProps) {
  return (
    <div>
      <h3>{product.name}</h3>
      <p>{product.price} MMK</p>
      {/* ... */}
    </div>
  );
}
```

## Filtering Lists

In the POS system, users filter products by category, availability, or price range. This is just array filtering before rendering:

```typescript
// ProductCatalog.tsx
import React, { useState } from 'react';
import ProductCard from './ProductCard';

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  category: string;
}

function ProductCatalog() {
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  
  const products: Product[] = [
    { id: '1', name: 'Mohinga', price: 3500, stock: 24, category: 'Breakfast' },
    { id: '2', name: 'Shan Noodles', price: 4000, stock: 15, category: 'Lunch' },
    { id: '3', name: 'Tea Leaf Salad', price: 3000, stock: 0, category: 'Appetizer' },
    { id: '4', name: 'Samosa', price: 1500, stock: 45, category: 'Snack' },
    { id: '5', name: 'Mandalay Noodles', price: 3800, stock: 12, category: 'Lunch' },
  ];
  
  // Filter products based on selected category
  const filteredProducts = selectedCategory === 'all' 
    ? products 
    : products.filter(p => p.category === selectedCategory);
  
  return (
    <div>
      <div className="category-filter">
        <button onClick={() => setSelectedCategory('all')}>All</button>
        <button onClick={() => setSelectedCategory('Breakfast')}>Breakfast</button>
        <button onClick={() => setSelectedCategory('Lunch')}>Lunch</button>
        <button onClick={() => setSelectedCategory('Appetizer')}>Appetizer</button>
        <button onClick={() => setSelectedCategory('Snack')}>Snack</button>
      </div>
      
      <div className="product-grid">
        {filteredProducts.map(product => (
          <ProductCard 
            key={product.id}
            product={product}
          />
        ))}
      </div>
      
      {filteredProducts.length === 0 && (
        <p>No products found in this category.</p>
      )}
    </div>
  );
}

export default ProductCatalog;
```

**The pattern:**

1. Store filter state (`selectedCategory`)
2. Filter the array before mapping: `products.filter(...)`
3. Map over the filtered array
4. Show empty state if no results

## Multiple Filters

Real-world apps often have multiple filters. Here's a more complex example from the POS:

```typescript
function ProductCatalog() {
  const [categoryFilter, setCategoryFilter] = useState<string>('all');
  const [stockFilter, setStockFilter] = useState<'all' | 'inStock' | 'lowStock'>('all');
  const [priceRange, setPriceRange] = useState<'all' | 'under3k' | '3k-5k' | 'over5k'>('all');
  
  const products: Product[] = [ /* ... */ ];
  
  // Chain filters
  let filteredProducts = products;
  
  // Category filter
  if (categoryFilter !== 'all') {
    filteredProducts = filteredProducts.filter(p => p.category === categoryFilter);
  }
  
  // Stock filter
  if (stockFilter === 'inStock') {
    filteredProducts = filteredProducts.filter(p => p.stock > 5);
  } else if (stockFilter === 'lowStock') {
    filteredProducts = filteredProducts.filter(p => p.stock > 0 && p.stock <= 5);
  }
  
  // Price filter
  if (priceRange === 'under3k') {
    filteredProducts = filteredProducts.filter(p => p.price < 3000);
  } else if (priceRange === '3k-5k') {
    filteredProducts = filteredProducts.filter(p => p.price >= 3000 && p.price <= 5000);
  } else if (priceRange === 'over5k') {
    filteredProducts = filteredProducts.filter(p => p.price > 5000);
  }
  
  return (
    <div>
      {/* Filter UI */}
      <div className="filters">
        {/* Category buttons */}
        {/* Stock buttons */}
        {/* Price buttons */}
      </div>
      
      <p>{filteredProducts.length} products found</p>
      
      <div className="product-grid">
        {filteredProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}
```

## Sorting Lists

In the POS admin panel, I needed to sort products by name, price, or stock level:

```typescript
function ProductCatalog() {
  const [sortBy, setSortBy] = useState<'name' | 'price' | 'stock'>('name');
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
  
  const products: Product[] = [ /* ... */ ];
  
  // Create a sorted copy (don't mutate original)
  const sortedProducts = [...products].sort((a, b) => {
    let comparison = 0;
    
    if (sortBy === 'name') {
      comparison = a.name.localeCompare(b.name);
    } else if (sortBy === 'price') {
      comparison = a.price - b.price;
    } else if (sortBy === 'stock') {
      comparison = a.stock - b.stock;
    }
    
    return sortOrder === 'asc' ? comparison : -comparison;
  });
  
  return (
    <div>
      <div className="sort-controls">
        <select onChange={(e) => setSortBy(e.target.value as any)}>
          <option value="name">Sort by Name</option>
          <option value="price">Sort by Price</option>
          <option value="stock">Sort by Stock</option>
        </select>
        
        <button onClick={() => setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc')}>
          {sortOrder === 'asc' ? '↑ Ascending' : '↓ Descending'}
        </button>
      </div>
      
      <div className="product-grid">
        {sortedProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}
```

**Important**: `[...products].sort()` creates a copy before sorting. Never mutate props or state directly!

## Nested Lists

Sometimes you need nested lists. In the POS, products are grouped by category:

```typescript
interface ProductsByCategory {
  category: string;
  products: Product[];
}

function GroupedProductCatalog() {
  const productsByCategory: ProductsByCategory[] = [
    {
      category: 'Breakfast',
      products: [
        { id: '1', name: 'Mohinga', price: 3500, stock: 24, category: 'Breakfast' },
        { id: '2', name: 'Shan Khauk Swe', price: 3200, stock: 18, category: 'Breakfast' },
      ]
    },
    {
      category: 'Lunch',
      products: [
        { id: '3', name: 'Shan Noodles', price: 4000, stock: 15, category: 'Lunch' },
        { id: '4', name: 'Mandalay Noodles', price: 3800, stock: 12, category: 'Lunch' },
      ]
    }
  ];
  
  return (
    <div>
      {productsByCategory.map(group => (
        <div key={group.category} className="category-section">
          <h2>{group.category}</h2>
          
          <div className="product-grid">
            {group.products.map(product => (
              <ProductCard 
                key={product.id} 
                product={product} 
              />
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}
```

**Key points:**

* Outer loop uses `category` as key
* Inner loop uses `product.id` as key
* Each level needs unique keys within its scope

## Conditional Rendering in Lists

Display different components based on data:

```typescript
function ProductList() {
  const products: Product[] = [ /* ... */ ];
  
  return (
    <div>
      {products.map(product => {
        // Out of stock? Show different component
        if (product.stock === 0) {
          return (
            <OutOfStockCard 
              key={product.id}
              product={product}
            />
          );
        }
        
        // Low stock? Show warning version
        if (product.stock < 5) {
          return (
            <LowStockProductCard 
              key={product.id}
              product={product}
            />
          );
        }
        
        // Default
        return (
          <ProductCard 
            key={product.id}
            product={product}
          />
        );
      })}
    </div>
  );
}
```

## Empty States

Always handle empty lists gracefully:

```typescript
function ProductCatalog() {
  const products: Product[] = [ /* ... */ ];
  
  if (products.length === 0) {
    return (
      <div className="empty-state">
        <h2>No Products Found</h2>
        <p>There are no products available at the moment.</p>
      </div>
    );
  }
  
  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
```

Or inline:

```typescript
<div className="product-grid">
  {products.length === 0 ? (
    <p>No products available.</p>
  ) : (
    products.map(product => (
      <ProductCard key={product.id} product={product} />
    ))
  )}
</div>
```

## Complete Example: POS Product Catalog

Here's the full implementation from the POS system:

```typescript
// ProductCatalog.tsx
import React, { useState } from 'react';
import ProductCard from './ProductCard';
import styles from './ProductCatalog.module.css';

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  category: string;
  imageUrl?: string;
}

function ProductCatalog() {
  const [categoryFilter, setCategoryFilter] = useState<string>('all');
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
  const [showOutOfStock, setShowOutOfStock] = useState(true);
  
  // In real app, this comes from API
  const products: Product[] = [
    { id: '1', name: 'Mohinga', price: 3500, stock: 24, category: 'Breakfast' },
    { id: '2', name: 'Shan Noodles', price: 4000, stock: 15, category: 'Lunch' },
    { id: '3', name: 'Tea Leaf Salad', price: 3000, stock: 0, category: 'Appetizer' },
    { id: '4', name: 'Samosa', price: 1500, stock: 45, category: 'Snack' },
    { id: '5', name: 'Mandalay Noodles', price: 3800, stock: 12, category: 'Lunch' },
    { id: '6', name: 'Shan Khao Swe', price: 3200, stock: 0, category: 'Breakfast' },
  ];
  
  // Get unique categories
  const categories = ['all', ...new Set(products.map(p => p.category))];
  
  // Filter products
  let filteredProducts = products;
  
  if (categoryFilter !== 'all') {
    filteredProducts = filteredProducts.filter(p => p.category === categoryFilter);
  }
  
  if (!showOutOfStock) {
    filteredProducts = filteredProducts.filter(p => p.stock > 0);
  }
  
  // Sort products
  const sortedProducts = [...filteredProducts].sort((a, b) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name);
    } else {
      return a.price - b.price;
    }
  });
  
  return (
    <div className={styles.container}>
      <div className={styles.controls}>
        <div className={styles.filters}>
          <label>Category:</label>
          {categories.map(category => (
            <button
              key={category}
              onClick={() => setCategoryFilter(category)}
              className={categoryFilter === category ? styles.active : ''}
            >
              {category}
            </button>
          ))}
        </div>
        
        <div className={styles.sortControls}>
          <label>Sort by:</label>
          <select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
            <option value="name">Name</option>
            <option value="price">Price</option>
          </select>
          
          <label>
            <input 
              type="checkbox" 
              checked={showOutOfStock}
              onChange={(e) => setShowOutOfStock(e.target.checked)}
            />
            Show out of stock items
          </label>
        </div>
      </div>
      
      <p className={styles.count}>{sortedProducts.length} products found</p>
      
      {sortedProducts.length === 0 ? (
        <div className={styles.emptyState}>
          <h2>No Products Found</h2>
          <p>Try adjusting your filters.</p>
        </div>
      ) : (
        <div className={styles.productGrid}>
          {sortedProducts.map(product => (
            <ProductCard 
              key={product.id}
              product={product}
            />
          ))}
        </div>
      )}
    </div>
  );
}

export default ProductCatalog;
```

## Key Learnings

### ✅ List Rendering Pattern

```typescript
{array.map(item => (
  <Component key={item.id} data={item} />
))}
```

### ✅ Keys Are Required

* Use unique, stable IDs
* Don't use array index (usually)
* Keys must be unique among siblings

### ✅ Filter Before Mapping

```typescript
const filtered = products.filter(p => p.category === 'Lunch');
return <div>{filtered.map(...)}</div>
```

### ✅ Sort Creates New Array

```typescript
const sorted = [...products].sort(...); // ✅ Creates copy
products.sort(...); // ❌ Mutates original
```

### ✅ Always Handle Empty States

* Show helpful message when `array.length === 0`
* Guide users on what to do next

## Common Mistakes I Made

**❌ Forgetting keys**

```typescript
{products.map(p => <ProductCard product={p} />)} // ⚠️ Warning
```

**❌ Using index as key with dynamic lists**

```typescript
{products.map((p, i) => <ProductCard key={i} {...p} />)} // ⚠️ Bugs
```

**❌ Mutating array before rendering**

```typescript
products.sort(...); // ❌ Mutates props/state
return products.map(...);
```

**❌ Not handling empty arrays**

```typescript
// No empty state = confused users
```

## Next Steps

We can now render dynamic lists of products, but users still can't search or filter by typing. In the POS system, the search bar is the most-used feature—users type product names to find items quickly.

Next, we'll learn about **forms and user input**, building a search interface with controlled components and validation.

**Continue to**: [Forms & User Input](https://blog.htunnthuthu.com/getting-started/programming/react-101/react-101-forms-input)
