# Context API

I'll never forget the moment when I had to pass the user's authentication state through seven levels of components just so a "Logout" button could access it. The `App` component had auth state, which went to `Layout`, then `Sidebar`, then `UserMenu`, then `MenuDropdown`, then `DropdownList`, and finally to `LogoutButton`. Six components that didn't care about auth, just passing it along like a relay race. That's when I learned about **Context**.

Context is React's built-in solution for sharing state globally without passing props through every level. It's perfect for data that many components need: theme settings, user authentication, shopping cart, language preferences. In the POS system, I use Context for cart state and user authentication—data that needs to be accessible anywhere in the app.

## When to Use Context vs Props

**Use props when**:

* Data is only needed by direct children
* The component hierarchy is shallow (1-2 levels)
* You want explicit data flow

**Use Context when**:

* Many components at different levels need the same data
* You're passing props through components that don't use them (prop drilling)
* The data is truly global (auth, theme, cart)

**In the POS system**:

* Product data → Props (only needed by product components)
* User authentication → Context (needed everywhere)
* Cart state → Context (needed by many components)
* Individual product quantity → Props (local to that component)

## Creating Your First Context: Cart

Let's solve the cart prop drilling problem with Context.

### Step 1: Create the Context

```typescript
// contexts/CartContext.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';

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

interface CartContextType {
  cart: CartItem[];
  addToCart: (product: { id: string; name: string; price: number }) => void;
  removeFromCart: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  itemCount: number;
  total: number;
}

// Create the context with undefined default
const CartContext = createContext<CartContextType | undefined>(undefined);

// Provider component
export function CartProvider({ children }: { children: ReactNode }) {
  const [cart, setCart] = useState<CartItem[]>([]);
  
  const addToCart = (product: { id: string; name: string; price: number }) => {
    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(prevCart => prevCart.filter(item => item.productId !== productId));
  };
  
  const updateQuantity = (productId: string, quantity: number) => {
    if (quantity <= 0) {
      removeFromCart(productId);
      return;
    }
    
    setCart(prevCart =>
      prevCart.map(item =>
        item.productId === productId
          ? { ...item, quantity }
          : item
      )
    );
  };
  
  const clearCart = () => {
    setCart([]);
  };
  
  // Derived values
  const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
  const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
  
  const value = {
    cart,
    addToCart,
    removeFromCart,
    updateQuantity,
    clearCart,
    itemCount,
    total
  };
  
  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

// Custom hook to use the cart context
export function useCart() {
  const context = useContext(CartContext);
  
  if (context === undefined) {
    throw new Error('useCart must be used within a CartProvider');
  }
  
  return context;
}
```

**What's happening here?**

1. **Create context**: `createContext<CartContextType>()`
2. **Provider component**: Manages state and provides value
3. **Custom hook**: Makes using context easier and safer

### Step 2: Wrap Your App with the Provider

```typescript
// App.tsx
import React from 'react';
import { CartProvider } from './contexts/CartContext';
import CheckoutPage from './pages/CheckoutPage';
import Header from './components/Header';

function App() {
  return (
    <CartProvider>
      <div className="app">
        <Header />
        <CheckoutPage />
      </div>
    </CartProvider>
  );
}

export default App;
```

**Everything inside `<CartProvider>` can now access cart state.**

### Step 3: Use the Context in Any Component

```typescript
// components/ProductCard.tsx
import React from 'react';
import { useCart } from '../contexts/CartContext';

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

interface ProductCardProps {
  product: Product;
}

function ProductCard({ product }: ProductCardProps) {
  const { addToCart } = useCart(); // ✅ No prop drilling!
  
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>{product.price} MMK</p>
      <button onClick={() => addToCart(product)}>
        Add to Cart
      </button>
    </div>
  );
}

export default ProductCard;
```

```typescript
// components/Header.tsx
import React from 'react';
import { useCart } from '../contexts/CartContext';

function Header() {
  const { itemCount, total } = useCart(); // ✅ Same context, different data
  
  return (
    <header>
      <h1>POS System</h1>
      <div className="cart-info">
        <span>Cart: {itemCount} items</span>
        <span>Total: {total.toLocaleString()} MMK</span>
      </div>
    </header>
  );
}

export default Header;
```

```typescript
// components/Cart.tsx
import React from 'react';
import { useCart } from '../contexts/CartContext';

function Cart() {
  const { cart, removeFromCart, updateQuantity, clearCart, total } = useCart();
  
  if (cart.length === 0) {
    return <div>Your cart is empty</div>;
  }
  
  return (
    <div className="cart">
      {cart.map(item => (
        <div key={item.productId} className="cart-item">
          <span>{item.name}</span>
          <input 
            type="number"
            value={item.quantity}
            onChange={(e) => updateQuantity(item.productId, parseInt(e.target.value))}
          />
          <span>{(item.price * item.quantity).toLocaleString()} MMK</span>
          <button onClick={() => removeFromCart(item.productId)}>Remove</button>
        </div>
      ))}
      
      <div className="cart-footer">
        <h3>Total: {total.toLocaleString()} MMK</h3>
        <button onClick={clearCart}>Clear Cart</button>
        <button>Checkout</button>
      </div>
    </div>
  );
}

export default Cart;
```

**No more prop drilling!** Any component can access cart state with `useCart()`.

## Creating Authentication Context

Authentication is another perfect use case for Context. Let's build it for the POS system.

```typescript
// contexts/AuthContext.tsx
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';

interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'cashier' | 'manager';
  tenantId: string;
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  hasRole: (role: string) => boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  
  // Check if user is already logged in (from localStorage or token)
  useEffect(() => {
    const checkAuth = async () => {
      try {
        const token = localStorage.getItem('authToken');
        
        if (!token) {
          setIsLoading(false);
          return;
        }
        
        const response = await fetch('/api/auth/me', {
          headers: {
            'Authorization': `Bearer ${token}`
          }
        });
        
        if (response.ok) {
          const userData = await response.json();
          setUser(userData);
        } else {
          localStorage.removeItem('authToken');
        }
      } catch (error) {
        console.error('Auth check failed:', error);
      } finally {
        setIsLoading(false);
      }
    };
    
    checkAuth();
  }, []);
  
  const login = async (email: string, password: string) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
      
      if (!response.ok) {
        throw new Error('Login failed');
      }
      
      const data = await response.json();
      
      localStorage.setItem('authToken', data.token);
      setUser(data.user);
    } catch (error) {
      console.error('Login error:', error);
      throw error;
    }
  };
  
  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };
  
  const hasRole = (role: string) => {
    return user?.role === role;
  };
  
  const value = {
    user,
    isAuthenticated: !!user,
    isLoading,
    login,
    logout,
    hasRole
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  
  return context;
}
```

### Using Auth Context

```typescript
// components/LoginForm.tsx
import React, { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    
    try {
      await login(email, password);
      // Redirect will happen in App component
    } catch (err) {
      setError('Invalid email or password');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h2>Login to POS System</h2>
      
      {error && <div className="error">{error}</div>}
      
      <input 
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      
      <input 
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      
      <button type="submit">Login</button>
    </form>
  );
}

export default LoginForm;
```

```typescript
// components/UserMenu.tsx
import React from 'react';
import { useAuth } from '../contexts/AuthContext';

function UserMenu() {
  const { user, logout } = useAuth();
  
  if (!user) return null;
  
  return (
    <div className="user-menu">
      <div className="user-info">
        <p>{user.name}</p>
        <p className="role">{user.role}</p>
      </div>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

export default UserMenu;
```

```typescript
// App.tsx - Combined providers
import React from 'react';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { CartProvider } from './contexts/CartContext';
import LoginForm from './components/LoginForm';
import CheckoutPage from './pages/CheckoutPage';

function AppContent() {
  const { isAuthenticated, isLoading } = useAuth();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  if (!isAuthenticated) {
    return <LoginForm />;
  }
  
  return <CheckoutPage />;
}

function App() {
  return (
    <AuthProvider>
      <CartProvider>
        <AppContent />
      </CartProvider>
    </AuthProvider>
  );
}

export default App;
```

## Composing Multiple Contexts

Real applications need multiple contexts. The pattern:

```typescript
// App.tsx
import { AuthProvider } from './contexts/AuthContext';
import { CartProvider } from './contexts/CartContext';
import { ThemeProvider } from './contexts/ThemeContext';
import { NotificationProvider } from './contexts/NotificationContext';

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          <NotificationProvider>
            <AppContent />
          </NotificationProvider>
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}
```

**Nesting can get deep.** Here's a cleaner pattern:

```typescript
// contexts/index.tsx
import React, { ReactNode } from 'react';
import { AuthProvider } from './AuthContext';
import { CartProvider } from './CartContext';
import { ThemeProvider } from './ThemeProvider';

export function AppProviders({ children }: { children: ReactNode }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          {children}
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// App.tsx
import { AppProviders } from './contexts';

function App() {
  return (
    <AppProviders>
      <AppContent />
    </AppProviders>
  );
}
```

## Context with Local Storage Persistence

Let's make the cart persist across page refreshes:

```typescript
// contexts/CartContext.tsx
export function CartProvider({ children }: { children: ReactNode }) {
  // Initialize from localStorage
  const [cart, setCart] = useState<CartItem[]>(() => {
    const savedCart = localStorage.getItem('cart');
    return savedCart ? JSON.parse(savedCart) : [];
  });
  
  // Save to localStorage whenever cart changes
  useEffect(() => {
    localStorage.setItem('cart', JSON.stringify(cart));
  }, [cart]);
  
  // ... rest of the provider
}
```

**Now the cart survives page refreshes!**

## Real POS Example: Complete Context Setup

Here's the production-quality setup I use in the POS system:

```typescript
// contexts/POSContext.tsx
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';

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

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

interface Order {
  id: string;
  items: CartItem[];
  total: number;
  createdAt: Date;
}

interface POSContextType {
  // Cart
  cart: CartItem[];
  addToCart: (product: Product) => void;
  removeFromCart: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  cartTotal: number;
  
  // Orders
  orders: Order[];
  createOrder: () => void;
  
  // UI State
  isCartOpen: boolean;
  toggleCart: () => void;
}

const POSContext = createContext<POSContextType | undefined>(undefined);

export function POSProvider({ children }: { children: ReactNode }) {
  // Load cart from localStorage
  const [cart, setCart] = useState<CartItem[]>(() => {
    const saved = localStorage.getItem('pos_cart');
    return saved ? JSON.parse(saved) : [];
  });
  
  // Load orders from localStorage
  const [orders, setOrders] = useState<Order[]>(() => {
    const saved = localStorage.getItem('pos_orders');
    return saved ? JSON.parse(saved) : [];
  });
  
  const [isCartOpen, setIsCartOpen] = useState(false);
  
  // Persist cart
  useEffect(() => {
    localStorage.setItem('pos_cart', JSON.stringify(cart));
  }, [cart]);
  
  // Persist orders
  useEffect(() => {
    localStorage.setItem('pos_orders', JSON.stringify(orders));
  }, [orders]);
  
  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
      }];
    });
    
    // Auto-open cart when item added
    setIsCartOpen(true);
  };
  
  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([]);
  };
  
  const cartTotal = cart.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  
  const createOrder = () => {
    if (cart.length === 0) return;
    
    const order: Order = {
      id: `ORD-${Date.now()}`,
      items: [...cart],
      total: cartTotal,
      createdAt: new Date()
    };
    
    setOrders(prev => [order, ...prev]);
    clearCart();
    setIsCartOpen(false);
    
    // Could also send to API here
    // await fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) });
  };
  
  const toggleCart = () => {
    setIsCartOpen(prev => !prev);
  };
  
  const value = {
    cart,
    addToCart,
    removeFromCart,
    updateQuantity,
    clearCart,
    cartTotal,
    orders,
    createOrder,
    isCartOpen,
    toggleCart
  };
  
  return (
    <POSContext.Provider value={value}>
      {children}
    </POSContext.Provider>
  );
}

export function usePOS() {
  const context = useContext(POSContext);
  
  if (context === undefined) {
    throw new Error('usePOS must be used within a POSProvider');
  }
  
  return context;
}
```

**Using it across the app**:

```typescript
// components/ProductCard.tsx
function ProductCard({ product }: { product: Product }) {
  const { addToCart } = usePOS();
  
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <button onClick={() => addToCart(product)}>Add to Cart</button>
    </div>
  );
}

// components/CartDrawer.tsx
function CartDrawer() {
  const { cart, isCartOpen, toggleCart, cartTotal, createOrder } = usePOS();
  
  if (!isCartOpen) return null;
  
  return (
    <div className="cart-drawer">
      <button onClick={toggleCart}>Close</button>
      {cart.map(item => (
        <div key={item.productId}>{item.name}</div>
      ))}
      <div>Total: {cartTotal} MMK</div>
      <button onClick={createOrder}>Complete Order</button>
    </div>
  );
}

// components/OrderHistory.tsx
function OrderHistory() {
  const { orders } = usePOS();
  
  return (
    <div className="orders">
      {orders.map(order => (
        <div key={order.id}>
          Order {order.id} - {order.total} MMK
        </div>
      ))}
    </div>
  );
}
```

## When NOT to Use Context

Context isn't always the answer. **Don't use Context for**:

### 1. Frequently Changing Values

```typescript
// ❌ Bad: Every keystroke re-renders all consumers
function SearchProvider({ children }) {
  const [searchTerm, setSearchTerm] = useState('');
  // All components using this context re-render on every keystroke!
}
```

**Better**: Keep fast-changing state local or use a state management library.

### 2. Simple Parent-Child Communication

```typescript
// ❌ Overkill: Just use props
function ParentComponent() {
  return (
    <ThemeProvider> {/* Unnecessary! */}
      <ChildComponent />
    </ThemeProvider>
  );
}
```

**Better**: Pass props directly.

### 3. Complex State Logic

```typescript
// ❌ Context getting too complex
const value = {
  users, setUsers,
  products, setProducts,
  orders, setOrders,
  // ... 20 more values
};
```

**Better**: Split into multiple contexts or use a state management library.

## Common Mistakes to Avoid

### 1. Creating Context Without Provider

```typescript
// ❌ Using context without provider
function App() {
  return <ProductCard />; // useCart() will fail!
}
```

**Fix**: Always wrap with provider.

### 2. Not Checking for Undefined

```typescript
export function useCart() {
  const context = useContext(CartContext);
  // ❌ Might be undefined if used outside provider
  return context;
}
```

**Fix**: Check and throw error:

```typescript
export function useCart() {
  const context = useContext(CartContext);
  
  if (context === undefined) {
    throw new Error('useCart must be used within CartProvider');
  }
  
  return context;
}
```

### 3. Creating New Objects Every Render

```typescript
function CartProvider({ children }) {
  const [cart, setCart] = useState([]);
  
  // ❌ New object every render - all consumers re-render!
  const value = {
    cart,
    addToCart: () => {},
    removeFromCart: () => {}
  };
  
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
```

**Fix**: Use useMemo or define functions outside render:

```typescript
function CartProvider({ children }) {
  const [cart, setCart] = useState([]);
  
  const addToCart = useCallback(() => {}, []);
  const removeFromCart = useCallback(() => {}, []);
  
  const value = useMemo(() => ({
    cart,
    addToCart,
    removeFromCart
  }), [cart, addToCart, removeFromCart]);
  
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
```

### 4. Too Many Contexts

```typescript
// ❌ Context explosion
<UserProvider>
  <ThemeProvider>
    <CartProvider>
      <OrdersProvider>
        <ProductsProvider>
          <CategoriesProvider>
            {/* Deeply nested! */}
          </CategoriesProvider>
        </ProductsProvider>
      </OrdersProvider>
    </CartProvider>
  </ThemeProvider>
</UserProvider>
```

**Fix**: Combine related contexts or use a state management library for complex apps.

## Key Learnings

1. **Context solves prop drilling** for global state
2. **Use Context for**: auth, theme, cart, language
3. **Don't use Context for**: frequent updates, simple parent-child communication
4. **Always provide a custom hook** (`useCart`) for type safety
5. **Wrap app with Provider** to make context available
6. **Check for undefined** in custom hooks
7. **Combine multiple contexts** by nesting providers
8. **Persist state** with localStorage + useEffect

## Next Steps

You now know how to share state globally with Context. But Context provides values, not reusable logic. What if you want to extract data fetching logic or cart operations into reusable functions? That's where **custom hooks** come in. In the next article, we'll build `useProducts`, `useCart`, and `useAuth` hooks to organize our POS system's logic.

Continue to: [Custom Hooks →](https://blog.htunnthuthu.com/getting-started/programming/react-101/react-101-custom-hooks)
