# Forms & User Input

The search bar in my POS system is the most critical feature. Restaurant staff need to find "Shan Noodles" among 200+ menu items instantly. Retail clerks search product codes while customers wait. A slow or buggy search interface directly impacts sales.

Building forms in React initially confused me. HTML forms have built-in state—the DOM tracks input values. But React wants to **control** that state. This is the concept of "controlled components," and once you understand it, forms become straightforward.

## The Problem: Uncontrolled vs Controlled

### Uncontrolled (Traditional HTML):

```typescript
function SearchBar() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const searchTerm = formData.get('search');
    console.log('Searching for:', searchTerm);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="search" placeholder="Search products..." />
      <button type="submit">Search</button>
    </form>
  );
}
```

This works, but React doesn't know the input value until form submission. You can't:

* Show live search results as user types
* Validate input in real-time
* Clear the input programmatically
* Disable submit based on input

### Controlled (React Way):

```typescript
import React, { useState } from 'react';

function SearchBar() {
  const [searchTerm, setSearchTerm] = useState('');
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log('Searching for:', searchTerm);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search products..." 
      />
      <button type="submit">Search</button>
      
      {searchTerm && <p>Searching for: {searchTerm}</p>}
    </form>
  );
}
```

**Now React controls the input:**

* `value={searchTerm}` sets the input value from state
* `onChange` updates state when user types
* State is the single source of truth
* We can access `searchTerm` anywhere, anytime

## Live Search: Real-Time Filtering

In the POS, search results appear instantly as users type. No submit button needed:

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

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

function ProductSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  
  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: 8, category: 'Appetizer' },
    { id: '4', name: 'Samosa', price: 1500, stock: 45, category: 'Snack' },
  ];
  
  // Filter products based on search term
  const filteredProducts = products.filter(product =>
    product.name.toLowerCase().includes(searchTerm.toLowerCase())
  );
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search products..."
        className="search-input"
      />
      
      <p>{filteredProducts.length} products found</p>
      
      <div className="product-grid">
        {filteredProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
      
      {searchTerm && filteredProducts.length === 0 && (
        <p className="no-results">No products match "{searchTerm}"</p>
      )}
    </div>
  );
}

export default ProductSearch;
```

**Every keystroke:**

1. User types → `onChange` fires
2. `setSearchTerm` updates state
3. Component re-renders
4. `filteredProducts` recalculates
5. UI updates instantly

## Multiple Form Fields

Real forms have multiple inputs. Here's an "Add Product" form from the POS admin:

```typescript
import React, { useState } from 'react';

interface ProductFormData {
  name: string;
  price: string;
  stock: string;
  category: string;
}

function AddProductForm() {
  const [formData, setFormData] = useState<ProductFormData>({
    name: '',
    price: '',
    stock: '',
    category: 'Breakfast',
  });
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log('Submitting:', formData);
    // In real app: API call to create product
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Product Name:</label>
        <input
          id="name"
          name="name"
          type="text"
          value={formData.name}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="price">Price (MMK):</label>
        <input
          id="price"
          name="price"
          type="number"
          value={formData.price}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="stock">Stock Quantity:</label>
        <input
          id="stock"
          name="stock"
          type="number"
          value={formData.stock}
          onChange={handleChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="category">Category:</label>
        <select
          id="category"
          name="category"
          value={formData.category}
          onChange={handleChange}
        >
          <option value="Breakfast">Breakfast</option>
          <option value="Lunch">Lunch</option>
          <option value="Dinner">Dinner</option>
          <option value="Snack">Snack</option>
          <option value="Appetizer">Appetizer</option>
        </select>
      </div>
      
      <button type="submit">Add Product</button>
    </form>
  );
}

export default AddProductForm;
```

**Key pattern:**

* Single `formData` object holds all field values
* Single `handleChange` function updates any field
* Use `name` attribute to identify which field changed
* `[name]: value` uses computed property names

## Form Validation

In production, validate before submission. Here's the POS product form with validation:

```typescript
import React, { useState } from 'react';

interface ProductFormData {
  name: string;
  price: string;
  stock: string;
  category: string;
}

interface FormErrors {
  name?: string;
  price?: string;
  stock?: string;
}

function AddProductForm() {
  const [formData, setFormData] = useState<ProductFormData>({
    name: '',
    price: '',
    stock: '',
    category: 'Breakfast',
  });
  
  const [errors, setErrors] = useState<FormErrors>({});
  const [touched, setTouched] = useState<Record<string, boolean>>({});
  
  const validate = (): boolean => {
    const newErrors: FormErrors = {};
    
    // Name validation
    if (!formData.name.trim()) {
      newErrors.name = 'Product name is required';
    } else if (formData.name.length < 3) {
      newErrors.name = 'Product name must be at least 3 characters';
    }
    
    // Price validation
    const price = parseFloat(formData.price);
    if (!formData.price) {
      newErrors.price = 'Price is required';
    } else if (isNaN(price) || price <= 0) {
      newErrors.price = 'Price must be a positive number';
    } else if (price > 1000000) {
      newErrors.price = 'Price seems unreasonably high';
    }
    
    // Stock validation
    const stock = parseInt(formData.stock);
    if (!formData.stock) {
      newErrors.stock = 'Stock quantity is required';
    } else if (isNaN(stock) || stock < 0) {
      newErrors.stock = 'Stock must be a non-negative number';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    
    // Clear error when user starts typing
    if (errors[name as keyof FormErrors]) {
      setErrors(prev => ({
        ...prev,
        [name]: undefined
      }));
    }
  };
  
  const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
    const { name } = e.target;
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));
  };
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    // Mark all fields as touched
    setTouched({
      name: true,
      price: true,
      stock: true,
    });
    
    if (validate()) {
      console.log('Valid! Submitting:', formData);
      // API call here
      
      // Reset form after success
      setFormData({ name: '', price: '', stock: '', category: 'Breakfast' });
      setErrors({});
      setTouched({});
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="product-form">
      <div className="form-field">
        <label htmlFor="name">Product Name:</label>
        <input
          id="name"
          name="name"
          type="text"
          value={formData.name}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.name && touched.name ? 'error' : ''}
        />
        {errors.name && touched.name && (
          <span className="error-message">{errors.name}</span>
        )}
      </div>
      
      <div className="form-field">
        <label htmlFor="price">Price (MMK):</label>
        <input
          id="price"
          name="price"
          type="number"
          value={formData.price}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.price && touched.price ? 'error' : ''}
        />
        {errors.price && touched.price && (
          <span className="error-message">{errors.price}</span>
        )}
      </div>
      
      <div className="form-field">
        <label htmlFor="stock">Stock Quantity:</label>
        <input
          id="stock"
          name="stock"
          type="number"
          value={formData.stock}
          onChange={handleChange}
          onBlur={handleBlur}
          className={errors.stock && touched.stock ? 'error' : ''}
        />
        {errors.stock && touched.stock && (
          <span className="error-message">{errors.stock}</span>
        )}
      </div>
      
      <div className="form-field">
        <label htmlFor="category">Category:</label>
        <select
          id="category"
          name="category"
          value={formData.category}
          onChange={handleChange}
        >
          <option value="Breakfast">Breakfast</option>
          <option value="Lunch">Lunch</option>
          <option value="Dinner">Dinner</option>
          <option value="Snack">Snack</option>
          <option value="Appetizer">Appetizer</option>
        </select>
      </div>
      
      <button type="submit">Add Product</button>
    </form>
  );
}

export default AddProductForm;
```

**Validation strategy:**

* **`errors`**: Stores error messages for each field
* **`touched`**: Tracks which fields user interacted with
* **`onBlur`**: Mark field as touched when user leaves it
* **Show errors only if**: Field has error AND user touched it
* **Clear errors**: When user starts typing again
* **Validate on submit**: Mark all fields touched, then validate

## Checkboxes and Radio Buttons

These work slightly differently. From the POS admin settings:

```typescript
function SettingsForm() {
  const [settings, setSettings] = useState({
    showOutOfStock: true,
    enableNotifications: false,
    theme: 'light',
  });
  
  const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, checked } = e.target;
    setSettings(prev => ({
      ...prev,
      [name]: checked // ← Use 'checked' not 'value'
    }));
  };
  
  const handleRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setSettings(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  return (
    <form>
      <div>
        <label>
          <input
            type="checkbox"
            name="showOutOfStock"
            checked={settings.showOutOfStock} // ← Use 'checked' not 'value'
            onChange={handleCheckboxChange}
          />
          Show out-of-stock products
        </label>
      </div>
      
      <div>
        <label>
          <input
            type="checkbox"
            name="enableNotifications"
            checked={settings.enableNotifications}
            onChange={handleCheckboxChange}
          />
          Enable low-stock notifications
        </label>
      </div>
      
      <div>
        <p>Theme:</p>
        <label>
          <input
            type="radio"
            name="theme"
            value="light"
            checked={settings.theme === 'light'}
            onChange={handleRadioChange}
          />
          Light
        </label>
        
        <label>
          <input
            type="radio"
            name="theme"
            value="dark"
            checked={settings.theme === 'dark'}
            onChange={handleRadioChange}
          />
          Dark
        </label>
      </div>
    </form>
  );
}
```

**Key differences:**

* **Checkboxes**: Use `checked` prop and `e.target.checked`
* **Radio buttons**: Use `checked={value === state}` pattern
* Both need `onChange` to be controlled

## Textarea

Textareas work like text inputs:

```typescript
function ProductDescription() {
  const [description, setDescription] = useState('');
  
  return (
    <div>
      <label htmlFor="description">Product Description:</label>
      <textarea
        id="description"
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        rows={5}
        placeholder="Enter product description..."
      />
      <p>{description.length} / 500 characters</p>
    </div>
  );
}
```

## Advanced: Search with Debouncing

In production, I debounced the search to avoid filtering on every keystroke. Here's a simpler pattern without external libraries:

```typescript
import React, { useState, useEffect } from 'react';

function ProductSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedTerm, setDebouncedTerm] = useState('');
  
  // Update debounced term after 300ms of no typing
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedTerm(searchTerm);
    }, 300);
    
    return () => clearTimeout(timer); // Cleanup on next keystroke
  }, [searchTerm]);
  
  const products = [ /* ... */ ];
  
  // Filter using debounced term
  const filteredProducts = products.filter(product =>
    product.name.toLowerCase().includes(debouncedTerm.toLowerCase())
  );
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search products..."
      />
      
      {searchTerm !== debouncedTerm && <p>Searching...</p>}
      
      <div className="product-grid">
        {filteredProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}
```

We'll cover `useEffect` in detail in the next article. For now, understand it delays the filter operation.

## Complete Example: Product Search & Filter Form

Combining everything—search, filters, sorting:

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

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

function ProductCatalog() {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('all');
  const [priceRange, setPriceRange] = useState({ min: '', max: '' });
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
  
  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: 8, category: 'Appetizer' },
    { id: '4', name: 'Samosa', price: 1500, stock: 45, category: 'Snack' },
    { id: '5', name: 'Mandalay Noodles', price: 3800, stock: 12, category: 'Lunch' },
  ];
  
  // Apply all filters
  let filtered = products;
  
  // Search filter
  if (searchTerm) {
    filtered = filtered.filter(p => 
      p.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }
  
  // Category filter
  if (selectedCategory !== 'all') {
    filtered = filtered.filter(p => p.category === selectedCategory);
  }
  
  // Price range filter
  const minPrice = parseFloat(priceRange.min) || 0;
  const maxPrice = parseFloat(priceRange.max) || Infinity;
  filtered = filtered.filter(p => p.price >= minPrice && p.price <= maxPrice);
  
  // Sort
  const sorted = [...filtered].sort((a, b) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name);
    } else {
      return a.price - b.price;
    }
  });
  
  const categories = ['all', ...new Set(products.map(p => p.category))];
  
  return (
    <div className="catalog">
      <div className="filters">
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search products..."
          className="search-input"
        />
        
        <select 
          value={selectedCategory}
          onChange={(e) => setSelectedCategory(e.target.value)}
        >
          {categories.map(cat => (
            <option key={cat} value={cat}>{cat}</option>
          ))}
        </select>
        
        <div className="price-range">
          <input
            type="number"
            value={priceRange.min}
            onChange={(e) => setPriceRange(prev => ({ ...prev, min: e.target.value }))}
            placeholder="Min price"
          />
          <span>-</span>
          <input
            type="number"
            value={priceRange.max}
            onChange={(e) => setPriceRange(prev => ({ ...prev, max: e.target.value }))}
            placeholder="Max price"
          />
        </div>
        
        <select 
          value={sortBy}
          onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}
        >
          <option value="name">Sort by Name</option>
          <option value="price">Sort by Price</option>
        </select>
      </div>
      
      <p>{sorted.length} products found</p>
      
      <div className="product-grid">
        {sorted.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

export default ProductCatalog;
```

## Key Learnings

### ✅ Controlled Components Pattern

```typescript
const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />
```

### ✅ Single Source of Truth

* State controls the input
* Input updates state
* Never let DOM and state diverge

### ✅ Form Submission

```typescript
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault(); // ← Prevent page reload
  // Process form data
};
```

### ✅ Validation Strategy

* Track errors in state
* Track touched fields
* Show errors only if touched
* Clear errors on typing

### ✅ Input Types

* **Text/Number/Select**: `value` + `onChange`
* **Checkbox**: `checked` + `onChange` (use `e.target.checked`)
* **Radio**: `checked={value === state}` pattern

## Common Mistakes I Made

**❌ Forgetting e.preventDefault()**

```typescript
const handleSubmit = (e) => {
  // Missing e.preventDefault()
  // Page reloads on submit ❌
};
```

**❌ Uncontrolled input (no value prop)**

```typescript
<input onChange={(e) => setValue(e.target.value)} /> // ❌ Uncontrolled
<input value={value} onChange={(e) => setValue(e.target.value)} /> // ✅ Controlled
```

**❌ Using value for checkboxes**

```typescript
<input type="checkbox" value={checked} /> // ❌ Wrong
<input type="checkbox" checked={checked} /> // ✅ Correct
```

**❌ Showing errors immediately**

```typescript
{errors.name && <span>{errors.name}</span>} // ❌ Shows before user types
{errors.name && touched.name && <span>{errors.name}</span>} // ✅ Only if touched
```

## Next Steps

Forms collect data, but where does it come from? In the POS system, product data comes from the Inventory Service API. I needed to fetch data when components mount, handle loading states, and manage errors.

Next, we'll learn **useEffect** for side effects like API calls, building a product catalog that loads data from a backend.

**Continue to**: [Effects & Data Fetching](https://blog.htunnthuthu.com/getting-started/programming/react-101/react-101-effects-data-fetching)
