# Effects & Data Fetching

The first time I tried to load products from my Inventory Service API into the POS frontend, I put the fetch call directly in my component's body. The page froze, my browser crashed, and I had made about 10,000 API requests in 3 seconds. That's when I learned that some code can't just run during render—you need **effects**.

Loading data from an API, subscribing to WebSockets, updating the document title—these are **side effects**. They interact with things outside React's rendering system. The `useEffect` hook is how you handle them safely.

## What Are Effects?

**An effect is code that runs&#x20;*****after*****&#x20;React finishes rendering.** It's for operations that shouldn't happen during rendering:

**In the POS system**:

* Fetching the product catalog from `/api/products` → Effect
* Updating the page title with cart count → Effect
* Connecting to inventory WebSocket for real-time updates → Effect
* Calculating the total price → NOT an effect (pure calculation)
* Rendering a product card → NOT an effect (that's just rendering)

**The rule**: If it talks to the outside world (API, browser API, timers), use an effect.

## Your First Effect: Fetching Products

Let's build what every POS system needs—loading products from an API.

### The Broken Approach (Don't Do This)

First, let's see what doesn't work:

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

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

function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  
  // ❌ NEVER DO THIS!
  fetch('/api/products')
    .then(res => res.json())
    .then(data => setProducts(data));
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}

export default ProductList;
```

**What happens?**

1. Component renders
2. Fetch runs, eventually calls `setProducts`
3. `setProducts` causes a re-render
4. Component renders again
5. Fetch runs again (goto step 2)
6. **Infinite loop! 💥**

### The Correct Approach with useEffect

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

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

function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  
  useEffect(() => {
    // This runs AFTER the first render
    fetch('/api/products')
      .then(res => res.json())
      .then(data => setProducts(data));
  }, []); // ← Empty dependency array = run once
  
  return (
    <div className="product-list">
      {products.map(product => (
        <div key={product.id} className="product-item">
          <h3>{product.name}</h3>
          <p>{product.price} MMK</p>
        </div>
      ))}
    </div>
  );
}

export default ProductList;
```

**Now it works!** The effect runs once after mount, fetches data, and updates state.

## Understanding useEffect

```typescript
useEffect(() => {
  // Effect code here
}, [dependencies]);
```

**Three parts**:

1. **Effect function**: The code that runs after render
2. **Dependency array**: Controls when the effect re-runs
3. **Cleanup function** (optional): Runs before the effect runs again or component unmounts

### The Dependency Array

This is the most important (and confusing) part of `useEffect`:

```typescript
// No dependency array - runs after EVERY render
useEffect(() => {
  console.log('Runs every render');
});

// Empty array - runs ONCE after first render  
useEffect(() => {
  console.log('Runs once on mount');
}, []);

// With dependencies - runs when those values change
useEffect(() => {
  console.log('Runs when category changes');
}, [category]);
```

**In the POS system**, I use different patterns:

```typescript
// Load products once on mount
useEffect(() => {
  fetchProducts();
}, []);

// Reload when category filter changes  
useEffect(() => {
  fetchProductsByCategory(category);
}, [category]);

// Update document title when cart changes
useEffect(() => {
  document.title = `Cart (${cartItems.length})`;
}, [cartItems]);
```

## Loading States: Building Production-Quality Data Fetching

A bare fetch isn't enough. Users need to know when data is loading and when errors occur.

### Complete Example with Loading and Error States

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

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

function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    // Reset states when starting fetch
    setLoading(true);
    setError(null);
    
    fetch('/api/products')
      .then(res => {
        if (!res.ok) {
          throw new Error(`HTTP error! status: ${res.status}`);
        }
        return res.json();
      })
      .then(data => {
        setProducts(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);
  
  if (loading) {
    return <div className="loading">Loading products...</div>;
  }
  
  if (error) {
    return (
      <div className="error">
        <p>Failed to load products: {error}</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }
  
  return (
    <div className="product-list">
      {products.length === 0 ? (
        <p>No products available</p>
      ) : (
        products.map(product => (
          <div key={product.id} 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>
            <span className="category">{product.category}</span>
          </div>
        ))
      )}
    </div>
  );
}

export default ProductList;
```

**Three states**:

1. **Loading**: Show a spinner or skeleton
2. **Error**: Show error message with retry option
3. **Success**: Show the data

This is the pattern I use in every component that fetches data.

## Using async/await (Better Syntax)

The then/catch chain works, but async/await is cleaner:

```typescript
function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    // Can't make useEffect callback async directly,
    // so we define an async function inside
    const fetchProducts = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch('/api/products');
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    
    fetchProducts();
  }, []);
  
  // ... rest of component
}
```

**Why the inner function?** `useEffect` expects a regular function or a function that returns a cleanup function. It can't handle a Promise (which async functions return).

## Cleanup Functions: Preventing Memory Leaks

When I added real-time inventory updates via WebSocket, I noticed the POS system kept opening new connections every time I navigated between pages. Each connection stayed open, leaking memory. **Cleanup functions** fixed this.

### Example: Subscribing to Real-Time Updates

```typescript
function ProductInventory({ productId }: { productId: string }) {
  const [stock, setStock] = useState<number>(0);
  
  useEffect(() => {
    // Create WebSocket connection
    const ws = new WebSocket('wss://api.example.com/inventory');
    
    ws.onopen = () => {
      ws.send(JSON.stringify({ 
        action: 'subscribe',
        productId 
      }));
    };
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.productId === productId) {
        setStock(data.stock);
      }
    };
    
    // Cleanup function - runs when component unmounts
    // or before effect runs again
    return () => {
      ws.close();
      console.log('WebSocket closed');
    };
  }, [productId]); // Re-run when productId changes
  
  return <div>Current stock: {stock}</div>;
}
```

**When cleanup runs**:

1. When the component unmounts (user navigates away)
2. Before the effect runs again (if productId changes)

**Common cleanup scenarios**:

* WebSocket connections → `ws.close()`
* Event listeners → `removeEventListener()`
* Timers → `clearTimeout()` / `clearInterval()`
* API requests → abort controller

### Canceling Fetch Requests

```typescript
function ProductDetails({ productId }: { productId: string }) {
  const [product, setProduct] = useState<Product | null>(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const abortController = new AbortController();
    
    const fetchProduct = async () => {
      try {
        setLoading(true);
        
        const response = await fetch(`/api/products/${productId}`, {
          signal: abortController.signal
        });
        
        const data = await response.json();
        setProduct(data);
      } catch (err) {
        // Ignore abort errors
        if (err instanceof Error && err.name !== 'AbortError') {
          console.error('Fetch error:', err);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchProduct();
    
    // Cleanup: abort the request if component unmounts
    // or productId changes before request completes
    return () => {
      abortController.abort();
    };
  }, [productId]);
  
  if (loading) return <div>Loading...</div>;
  if (!product) return <div>Product not found</div>;
  
  return (
    <div className="product-details">
      <h1>{product.name}</h1>
      <p>{product.price} MMK</p>
    </div>
  );
}
```

**Why this matters**: Users click around fast. Without cleanup, old requests might complete after newer ones, showing stale data.

## Dependency Array Deep Dive

This is where most bugs happen. Let's understand it thoroughly.

### Missing Dependencies

```typescript
function ProductSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [products, setProducts] = useState<Product[]>([]);
  
  useEffect(() => {
    // ❌ Bug: searchTerm is used but not in dependencies
    fetch(`/api/products?search=${searchTerm}`)
      .then(res => res.json())
      .then(data => setProducts(data));
  }, []); // Empty array means this never re-runs!
  
  return (
    <div>
      <input 
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      {/* Products never update when searchTerm changes */}
    </div>
  );
}
```

**Fix**: Add `searchTerm` to dependencies:

```typescript
useEffect(() => {
  fetch(`/api/products?search=${searchTerm}`)
    .then(res => res.json())
    .then(data => setProducts(data));
}, [searchTerm]); // ✅ Re-run when searchTerm changes
```

### Too Many Dependencies (Infinite Loops)

```typescript
function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [filters, setFilters] = useState({ category: 'all', priceRange: 'all' });
  
  useEffect(() => {
    fetch(`/api/products?category=${filters.category}&price=${filters.priceRange}`)
      .then(res => res.json())
      .then(data => setProducts(data));
  }, [filters]); // ❌ Problem: filters is a new object every render
  
  // Somewhere in component:
  // setFilters({ category: 'all', priceRange: 'all' }); // New object reference!
}
```

**Fix option 1**: Depend on individual values:

```typescript
useEffect(() => {
  fetch(`/api/products?category=${filters.category}&price=${filters.priceRange}`)
    .then(res => res.json())
    .then(data => setProducts(data));
}, [filters.category, filters.priceRange]); // ✅ Primitive values
```

**Fix option 2**: Use separate state:

```typescript
const [category, setCategory] = useState('all');
const [priceRange, setPriceRange] = useState('all');

useEffect(() => {
  fetch(`/api/products?category=${category}&price=${priceRange}`)
    .then(res => res.json())
    .then(data => setProducts(data));
}, [category, priceRange]); // ✅ Works perfectly
```

## Real POS Example: Product Catalog with Filters

Here's how I built the main product listing page with category filtering:

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

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

interface ProductCatalogProps {
  tenantId: string;
}

function ProductCatalog({ tenantId }: ProductCatalogProps) {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [selectedCategory, setSelectedCategory] = useState<string>('all');
  const [categories, setCategories] = useState<string[]>([]);
  
  // Fetch categories once on mount
  useEffect(() => {
    const fetchCategories = async () => {
      try {
        const response = await fetch(`/api/tenants/${tenantId}/categories`);
        const data = await response.json();
        setCategories(['all', ...data]);
      } catch (err) {
        console.error('Failed to fetch categories:', err);
      }
    };
    
    fetchCategories();
  }, [tenantId]);
  
  // Fetch products when category or tenant changes
  useEffect(() => {
    const fetchProducts = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const url = selectedCategory === 'all'
          ? `/api/tenants/${tenantId}/products`
          : `/api/tenants/${tenantId}/products?category=${selectedCategory}`;
        
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error('Failed to fetch products');
        }
        
        const data = await response.json();
        setProducts(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Unknown error');
      } finally {
        setLoading(false);
      }
    };
    
    fetchProducts();
  }, [tenantId, selectedCategory]); // Re-fetch when either changes
  
  return (
    <div className="product-catalog">
      <div className="filters">
        <h2>Categories</h2>
        {categories.map(category => (
          <button
            key={category}
            onClick={() => setSelectedCategory(category)}
            className={selectedCategory === category ? 'active' : ''}
          >
            {category}
          </button>
        ))}
      </div>
      
      <div className="products">
        {loading && <div className="loading">Loading products...</div>}
        
        {error && (
          <div className="error">
            <p>Error: {error}</p>
            <button onClick={() => window.location.reload()}>Retry</button>
          </div>
        )}
        
        {!loading && !error && products.length === 0 && (
          <div className="empty">
            <p>No products found in this category</p>
          </div>
        )}
        
        {!loading && !error && products.length > 0 && (
          <div className="product-grid">
            {products.map(product => (
              <div key={product.id} 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">
                  {product.stock > 0 ? `In Stock: ${product.stock}` : 'Out of Stock'}
                </p>
                <span className="category-badge">{product.category}</span>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

export default ProductCatalog;
```

**What's happening**:

1. Two separate effects for two separate concerns
2. Categories load once (depends on `tenantId`)
3. Products reload when category filter or tenant changes
4. Full loading/error/empty/success state handling
5. Clean separation of responsibilities

## Common Mistakes to Avoid

### 1. Forgetting the Dependency Array

```typescript
// ❌ Runs after every render
useEffect(() => {
  fetch('/api/products')
    .then(res => res.json())
    .then(data => setProducts(data));
}); // No dependency array!
```

**Fix**: Always provide a dependency array, even if empty.

### 2. Modifying State in Render

```typescript
function BadComponent() {
  const [count, setCount] = useState(0);
  
  // ❌ NEVER DO THIS
  setCount(count + 1); // Infinite loop!
  
  return <div>{count}</div>;
}
```

**Fix**: Move state updates to effects or event handlers.

### 3. Not Handling Cleanup

```typescript
useEffect(() => {
  const interval = setInterval(() => {
    fetchLatestData();
  }, 5000);
  
  // ❌ Missing cleanup - interval keeps running after unmount
}, []);
```

**Fix**: Return cleanup function:

```typescript
useEffect(() => {
  const interval = setInterval(() => {
    fetchLatestData();
  }, 5000);
  
  return () => clearInterval(interval); // ✅
}, []);
```

### 4. Fetching in a Loop

```typescript
function ProductList({ productIds }: { productIds: string[] }) {
  const [products, setProducts] = useState<Product[]>([]);
  
  useEffect(() => {
    // ❌ Makes one request per product
    productIds.forEach(id => {
      fetch(`/api/products/${id}`)
        .then(res => res.json())
        .then(product => setProducts(prev => [...prev, product]));
    });
  }, [productIds]);
}
```

**Fix**: Fetch in one request:

```typescript
useEffect(() => {
  const fetchProducts = async () => {
    const ids = productIds.join(',');
    const response = await fetch(`/api/products?ids=${ids}`);
    const data = await response.json();
    setProducts(data);
  };
  
  fetchProducts();
}, [productIds]);
```

### 5. Stale Closures

```typescript
function Timer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1); // ❌ Always uses initial count (0)
    }, 1000);
    
    return () => clearInterval(interval);
  }, []); // Empty dependencies = count is always 0
  
  return <div>{count}</div>;
}
```

**Fix**: Use functional update:

```typescript
useEffect(() => {
  const interval = setInterval(() => {
    setCount(prevCount => prevCount + 1); // ✅ Uses current value
  }, 1000);
  
  return () => clearInterval(interval);
}, []);
```

## Key Learnings

1. **Effects run after render**, not during
2. **Dependency array** controls when effects re-run
3. **Empty array** `[]` means run once on mount
4. **Cleanup functions** prevent memory leaks
5. **Loading/error states** are essential for good UX
6. **Abort fetch requests** when dependencies change
7. **Never call setState** directly in render
8. **Use async/await** inside effects for cleaner code

## Next Steps

Now you can fetch data and handle side effects. But what happens when you need to pass data between components? In the next article, we'll explore **component communication**—how to pass data down with props and callbacks up to manage state across multiple components in the POS cart system.

Continue to: [Component Communication →](https://blog.htunnthuthu.com/getting-started/programming/react-101/react-101-component-communication)
