# Component Communication

Building the shopping cart for my POS system taught me a crucial lesson: components don't exist in isolation. When a user clicks "Add to Cart" on a product card, the cart component needs to know about it. When the cart updates, the header showing the item count needs to reflect the change. This is **component communication**—the art of making components talk to each other.

I started by putting all state in every component. The cart had its own state, each product card had its own state, the header had its own state. Nothing was synchronized. The cart count showed 5, but the cart itself was empty. It was chaos. Then I learned: **data flows down, events flow up.**

## The Fundamental Pattern: Props Down, Callbacks Up

This is the core of React's architecture:

* **Props** carry data from parent to child (down the tree)
* **Callbacks** carry events from child to parent (up the tree)
* **State** lives in the parent that needs to coordinate children

In the POS system:

* Parent (`CheckoutPage`) manages cart state
* Child (`ProductCard`) receives product data via props
* Child calls `onAddToCart` callback to notify parent
* Parent updates state, all children re-render with new data

## Props: Passing Data Down

We've seen props before, but let's understand them deeply.

### Basic Props Example

```typescript
// ProductCard.tsx
interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  imageUrl: string;
}

interface ProductCardProps {
  product: Product;
}

function ProductCard({ product }: ProductCardProps) {
  return (
    <div className="product-card">
      <img src={product.imageUrl} alt={product.name} />
      <h3>{product.name}</h3>
      <p className="price">{product.price} MMK</p>
      <p className="stock">Stock: {product.stock}</p>
    </div>
  );
}

export default ProductCard;
```

**Using it**:

```typescript
// ProductList.tsx
function ProductList() {
  const products = [
    { id: '1', name: 'Mohinga', price: 3500, stock: 24, imageUrl: '/mohinga.jpg' },
    { id: '2', name: 'Shan Noodles', price: 4000, stock: 18, imageUrl: '/shan.jpg' }
  ];
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}
```

**Data flows down**: `ProductList` → `ProductCard`

### Props Are Read-Only

```typescript
function ProductCard({ product }: ProductCardProps) {
  // ❌ NEVER DO THIS
  product.price = product.price * 1.1; // Mutating props!
  
  return <div>{product.name}</div>;
}
```

**Why?** Props belong to the parent. Only the parent should change them.

**If you need to modify data**:

```typescript
function ProductCard({ product }: ProductCardProps) {
  // ✅ Create a new value
  const priceWithTax = product.price * 1.1;
  
  return (
    <div>
      <h3>{product.name}</h3>
      <p>Price: {product.price} MMK</p>
      <p>With Tax: {priceWithTax} MMK</p>
    </div>
  );
}
```

## Callbacks: Events Flow Up

Props go down. How do we send information back up? **Callbacks**.

### Adding Products to Cart

```typescript
// ProductCard.tsx
interface ProductCardProps {
  product: Product;
  onAddToCart: (productId: string) => void; // Callback prop
}

function ProductCard({ product, onAddToCart }: ProductCardProps) {
  const handleClick = () => {
    console.log('Button clicked!');
    onAddToCart(product.id); // Call the callback
  };
  
  return (
    <div className="product-card">
      <img src={product.imageUrl} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price} MMK</p>
      <button onClick={handleClick}>Add to Cart</button>
    </div>
  );
}
```

**Parent component**:

```typescript
// ProductList.tsx
function ProductList() {
  const products = [...]; // Array of products
  
  const handleAddToCart = (productId: string) => {
    console.log(`Adding product ${productId} to cart`);
    // Update cart state (we'll add this next)
  };
  
  return (
    <div>
      {products.map(product => (
        <ProductCard 
          key={product.id}
          product={product}
          onAddToCart={handleAddToCart} // Pass callback down
        />
      ))}
    </div>
  );
}
```

**Event flows up**: `ProductCard` → `ProductList`

### Naming Convention

* Props that are callbacks start with `on`: `onAddToCart`, `onClick`, `onSubmit`
* Handler functions start with `handle`: `handleAddToCart`, `handleClick`, `handleSubmit`

```typescript
<ProductCard 
  onAddToCart={handleAddToCart}    // ✅ Good
  addToCart={doAddToCart}          // ❌ Confusing
  onClick={clickHandler}           // ❌ Inconsistent
/>
```

## Lifting State Up

Here's where it gets interesting. When two components need to share state, **lift the state to their common parent**.

### Problem: Disconnected Components

```typescript
// ❌ Bad: Each component has its own cart state

function ProductCard() {
  const [cart, setCart] = useState([]);
  // Add to local cart
}

function CartSummary() {
  const [cart, setCart] = useState([]);
  // Shows different cart!
}

function Header() {
  const [cart, setCart] = useState([]);
  // Shows yet another cart!
}
```

These three components don't share state. Each has its own cart.

### Solution: Lift State to Common Parent

```typescript
// App.tsx - The parent that contains all three components
import React, { useState } from 'react';

interface CartItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

function App() {
  // ✅ State lives here, at the common parent
  const [cart, setCart] = useState<CartItem[]>([]);
  
  const handleAddToCart = (product: Product) => {
    setCart(prevCart => {
      // Check if product already in cart
      const existingItem = prevCart.find(item => item.productId === product.id);
      
      if (existingItem) {
        // Increase quantity
        return prevCart.map(item =>
          item.productId === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        // Add new item
        return [...prevCart, {
          productId: product.id,
          name: product.name,
          price: product.price,
          quantity: 1
        }];
      }
    });
  };
  
  const handleRemoveFromCart = (productId: string) => {
    setCart(prevCart => prevCart.filter(item => item.productId !== productId));
  };
  
  const handleUpdateQuantity = (productId: string, quantity: number) => {
    if (quantity <= 0) {
      handleRemoveFromCart(productId);
      return;
    }
    
    setCart(prevCart =>
      prevCart.map(item =>
        item.productId === productId
          ? { ...item, quantity }
          : item
      )
    );
  };
  
  return (
    <div className="app">
      <Header cartItemCount={cart.length} />
      
      <ProductList onAddToCart={handleAddToCart} />
      
      <CartSummary 
        cart={cart}
        onRemoveFromCart={handleRemoveFromCart}
        onUpdateQuantity={handleUpdateQuantity}
      />
    </div>
  );
}
```

**Now all components share the same cart state** through props and callbacks.

## Building the Cart System

Let's build a complete, production-quality cart for the POS system.

### The Parent Component (State Manager)

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

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

interface CartItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

function CheckoutPage() {
  const [products] = useState<Product[]>([
    { id: '1', name: 'Mohinga', price: 3500, stock: 24, category: 'Breakfast', imageUrl: '/mohinga.jpg' },
    { id: '2', name: 'Shan Noodles', price: 4000, stock: 18, category: 'Lunch', imageUrl: '/shan.jpg' },
    { id: '3', name: 'Tea Leaf Salad', price: 5500, stock: 12, category: 'Salad', imageUrl: '/salad.jpg' }
  ]);
  
  const [cart, setCart] = useState<CartItem[]>([]);
  
  // Add to cart or increase quantity
  const addToCart = (product: Product) => {
    setCart(prevCart => {
      const existingItem = prevCart.find(item => item.productId === product.id);
      
      if (existingItem) {
        // Product already in cart - increase quantity
        return prevCart.map(item =>
          item.productId === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        // New product - add to cart
        return [...prevCart, {
          productId: product.id,
          name: product.name,
          price: product.price,
          quantity: 1
        }];
      }
    });
  };
  
  // Remove item from cart completely
  const removeFromCart = (productId: string) => {
    setCart(prevCart => prevCart.filter(item => item.productId !== productId));
  };
  
  // Update quantity (can increase or decrease)
  const updateQuantity = (productId: string, newQuantity: number) => {
    if (newQuantity <= 0) {
      removeFromCart(productId);
      return;
    }
    
    setCart(prevCart =>
      prevCart.map(item =>
        item.productId === productId
          ? { ...item, quantity: newQuantity }
          : item
      )
    );
  };
  
  // Calculate total
  const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  
  return (
    <div className="checkout-page">
      <div className="products-section">
        <h2>Products</h2>
        <ProductGrid 
          products={products}
          onAddToCart={addToCart}
        />
      </div>
      
      <div className="cart-section">
        <h2>Cart ({cart.length} items)</h2>
        <Cart 
          items={cart}
          onRemove={removeFromCart}
          onUpdateQuantity={updateQuantity}
          total={total}
        />
      </div>
    </div>
  );
}

export default CheckoutPage;
```

### The ProductGrid Child Component

```typescript
// ProductGrid.tsx
interface ProductGridProps {
  products: Product[];
  onAddToCart: (product: Product) => void;
}

function ProductGrid({ products, onAddToCart }: ProductGridProps) {
  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard 
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
}

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

function ProductCard({ product, onAddToCart }: ProductCardProps) {
  return (
    <div className="product-card">
      <img src={product.imageUrl} alt={product.name} />
      <h3>{product.name}</h3>
      <p className="price">{product.price.toLocaleString()} MMK</p>
      <p className="stock">Stock: {product.stock}</p>
      <span className="category">{product.category}</span>
      <button 
        onClick={() => onAddToCart(product)}
        disabled={product.stock === 0}
      >
        {product.stock > 0 ? 'Add to Cart' : 'Out of Stock'}
      </button>
    </div>
  );
}
```

### The Cart Child Component

```typescript
// Cart.tsx
interface CartProps {
  items: CartItem[];
  onRemove: (productId: string) => void;
  onUpdateQuantity: (productId: string, quantity: number) => void;
  total: number;
}

function Cart({ items, onRemove, onUpdateQuantity, total }: CartProps) {
  if (items.length === 0) {
    return (
      <div className="empty-cart">
        <p>Your cart is empty</p>
      </div>
    );
  }
  
  return (
    <div className="cart">
      <div className="cart-items">
        {items.map(item => (
          <CartItem 
            key={item.productId}
            item={item}
            onRemove={onRemove}
            onUpdateQuantity={onUpdateQuantity}
          />
        ))}
      </div>
      
      <div className="cart-total">
        <h3>Total: {total.toLocaleString()} MMK</h3>
        <button className="checkout-button">Checkout</button>
      </div>
    </div>
  );
}

// CartItem.tsx
interface CartItemProps {
  item: CartItem;
  onRemove: (productId: string) => void;
  onUpdateQuantity: (productId: string, quantity: number) => void;
}

function CartItem({ item, onRemove, onUpdateQuantity }: CartItemProps) {
  const handleIncrease = () => {
    onUpdateQuantity(item.productId, item.quantity + 1);
  };
  
  const handleDecrease = () => {
    onUpdateQuantity(item.productId, item.quantity - 1);
  };
  
  const handleRemove = () => {
    onRemove(item.productId);
  };
  
  const itemTotal = item.price * item.quantity;
  
  return (
    <div className="cart-item">
      <div className="item-details">
        <h4>{item.name}</h4>
        <p className="price">{item.price.toLocaleString()} MMK</p>
      </div>
      
      <div className="quantity-controls">
        <button onClick={handleDecrease}>-</button>
        <span>{item.quantity}</span>
        <button onClick={handleIncrease}>+</button>
      </div>
      
      <div className="item-total">
        {itemTotal.toLocaleString()} MMK
      </div>
      
      <button onClick={handleRemove} className="remove-button">
        Remove
      </button>
    </div>
  );
}
```

**Data flow**:

1. User clicks "Add to Cart" on `ProductCard`
2. `ProductCard` calls `onAddToCart(product)`
3. Callback bubbles up to `CheckoutPage`
4. `CheckoutPage` updates cart state
5. New cart state flows down as props to `Cart`
6. `Cart` re-renders with new items

## Sharing State Between Siblings

Components at the same level (siblings) can't pass data directly. They must communicate through a shared parent.

### Problem: Cart Count in Header

The `Header` needs to show cart item count, but the cart is managed by `CheckoutPage`.

```typescript
// App.tsx
function App() {
  const [cart, setCart] = useState<CartItem[]>([]);
  
  return (
    <div>
      {/* Header and CheckoutPage are siblings */}
      <Header cartCount={cart.length} />
      <CheckoutPage cart={cart} setCart={setCart} />
    </div>
  );
}
```

**Solution**: Lift cart state to `App`, the common parent. Now both children can access it.

### Real Example: Search and Filter

In the POS, I have a search bar in the header and product grid in the body. They're siblings, but filtering needs to work.

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

function App() {
  const [products] = useState<Product[]>([/* ... */]);
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('all');
  
  // Filter products based on search and category
  const filteredProducts = products.filter(product => {
    const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase());
    const matchesCategory = selectedCategory === 'all' || product.category === selectedCategory;
    return matchesSearch && matchesCategory;
  });
  
  return (
    <div className="app">
      <Header 
        searchTerm={searchTerm}
        onSearchChange={setSearchTerm}
        selectedCategory={selectedCategory}
        onCategoryChange={setSelectedCategory}
      />
      
      <ProductGrid products={filteredProducts} />
    </div>
  );
}

// Header.tsx
interface HeaderProps {
  searchTerm: string;
  onSearchChange: (value: string) => void;
  selectedCategory: string;
  onCategoryChange: (value: string) => void;
}

function Header({ searchTerm, onSearchChange, selectedCategory, onCategoryChange }: HeaderProps) {
  return (
    <header>
      <input 
        type="text"
        value={searchTerm}
        onChange={(e) => onSearchChange(e.target.value)}
        placeholder="Search products..."
      />
      
      <select 
        value={selectedCategory}
        onChange={(e) => onCategoryChange(e.target.value)}
      >
        <option value="all">All Categories</option>
        <option value="Breakfast">Breakfast</option>
        <option value="Lunch">Lunch</option>
        <option value="Salad">Salad</option>
      </select>
    </header>
  );
}
```

**How it works**:

1. State lives in `App` (common parent)
2. `Header` receives current values and callbacks
3. `ProductGrid` receives filtered results
4. User types in search → `Header` calls `onSearchChange` → `App` updates state → `ProductGrid` gets new filtered products

## Prop Drilling: The Problem We'll Solve Next

As applications grow, passing props through many levels becomes tedious:

```typescript
// ❌ Prop drilling - passing cart through many levels

function App() {
  const [cart, setCart] = useState([]);
  
  return <Dashboard cart={cart} setCart={setCart} />;
}

function Dashboard({ cart, setCart }) {
  return <Sidebar cart={cart} setCart={setCart} />;
}

function Sidebar({ cart, setCart }) {
  return <UserMenu cart={cart} setCart={setCart} />;
}

function UserMenu({ cart, setCart }) {
  return <CartBadge cart={cart} />;
}

function CartBadge({ cart }) {
  return <span>{cart.length}</span>;
}
```

We're passing `cart` through 4 components just so `CartBadge` can use it. The middle components don't even care about cart—they're just passing it along.

**Solution**: Context API (next article!)

## Complete POS Checkout Flow

Here's the full working example showing all patterns together:

```typescript
// CheckoutPage.tsx - Complete implementation
import React, { useState, useEffect } from 'react';

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

interface CartItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

function CheckoutPage() {
  const [products, setProducts] = useState<Product[]>([]);
  const [cart, setCart] = useState<CartItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('all');
  
  // Load products
  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const response = await fetch('/api/products');
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        console.error('Failed to load products:', err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchProducts();
  }, []);
  
  // Filter products
  const filteredProducts = products.filter(product => {
    const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase());
    const matchesCategory = selectedCategory === 'all' || product.category === selectedCategory;
    return matchesSearch && matchesCategory;
  });
  
  // Cart operations
  const addToCart = (product: Product) => {
    setCart(prevCart => {
      const existing = prevCart.find(item => item.productId === product.id);
      if (existing) {
        return prevCart.map(item =>
          item.productId === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prevCart, {
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      }];
    });
  };
  
  const removeFromCart = (productId: string) => {
    setCart(prev => prev.filter(item => item.productId !== productId));
  };
  
  const updateQuantity = (productId: string, quantity: number) => {
    if (quantity <= 0) {
      removeFromCart(productId);
      return;
    }
    setCart(prev =>
      prev.map(item =>
        item.productId === productId ? { ...item, quantity } : item
      )
    );
  };
  
  const clearCart = () => {
    setCart([]);
  };
  
  // Calculate totals
  const subtotal = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const tax = subtotal * 0.05;
  const total = subtotal + tax;
  
  if (loading) {
    return <div>Loading products...</div>;
  }
  
  return (
    <div className="checkout-page">
      <CheckoutHeader 
        cartItemCount={cart.length}
        searchTerm={searchTerm}
        onSearchChange={setSearchTerm}
        selectedCategory={selectedCategory}
        onCategoryChange={setSelectedCategory}
      />
      
      <div className="checkout-body">
        <ProductSection 
          products={filteredProducts}
          onAddToCart={addToCart}
        />
        
        <CartSection 
          cart={cart}
          onRemove={removeFromCart}
          onUpdateQuantity={updateQuantity}
          onClearCart={clearCart}
          subtotal={subtotal}
          tax={tax}
          total={total}
        />
      </div>
    </div>
  );
}

export default CheckoutPage;
```

**This demonstrates**:

* State lifted to parent (`CheckoutPage`)
* Props flowing down to children
* Callbacks flowing up from children
* Sibling communication through parent
* Derived state (filtered products, totals)
* Multiple callbacks for different operations

## Common Mistakes to Avoid

### 1. Not Lifting State High Enough

```typescript
// ❌ Cart state too low - Header can't access it
function ProductPage() {
  return (
    <>
      <Header /> {/* Needs cart count but can't get it */}
      <ProductList /> {/* Has cart state */}
    </>
  );
}
```

**Fix**: Lift cart state to parent of both components.

### 2. Passing Too Many Props

```typescript
// ❌ Props becoming unmanageable
<Cart 
  items={cart}
  onAdd={handleAdd}
  onRemove={handleRemove}
  onUpdate={handleUpdate}
  onClear={handleClear}
  subtotal={subtotal}
  tax={tax}
  total={total}
  discount={discount}
  currency={currency}
  // ... 15 more props
/>
```

**Fix**: Group related props or use Context (next article).

### 3. Mutating Props

```typescript
function ProductCard({ product }) {
  // ❌ Mutating props
  product.price = product.price * 1.1;
  
  return <div>{product.name}</div>;
}
```

**Fix**: Create new values, don't mutate props.

### 4. Callback Hell

```typescript
// ❌ Too many nested callbacks
<ProductCard 
  onAddToCart={(product) => {
    handleAddToCart(product);
    showNotification();
    updateAnalytics();
    logToServer();
  }}
/>
```

**Fix**: Extract to a named function in parent component.

## Key Learnings

1. **Props flow down** from parent to child
2. **Callbacks flow up** from child to parent
3. **State should live** in the lowest common ancestor
4. **Props are read-only** - never mutate them
5. **Siblings communicate** through their parent
6. **Derived state** (calculated values) doesn't need useState
7. **Lift state up** when multiple components need it
8. **Name callbacks** with `on` prefix, handlers with `handle` prefix

## Next Steps

You've mastered component communication, but passing props through many levels gets tedious. In the next article, we'll explore the **Context API**—React's built-in solution for sharing state globally without prop drilling. We'll use it to build cart and authentication contexts for the POS system.

Continue to: [Context API →](https://blog.htunnthuthu.com/getting-started/programming/react-101/react-101-context-api)
