🔥 0
0
Lesson 9 of 10 25 min +125 XP

TypeScript with React

React + TypeScript is the industry standard. Let's build type-safe e-commerce components.

Typing Component Props

Basic Props

// Define props interface
interface ProductCardProps {
  name: string;
  price: number;
  imageUrl: string;
  inStock: boolean;
}

// Use in component
function ProductCard({ name, price, imageUrl, inStock }: ProductCardProps) {
  return (
    <div className="product-card">
      <img src={imageUrl} alt={name} />
      <h3>{name}</h3>
      <p>${price.toFixed(2)}</p>
      {!inStock && <span className="badge">Out of Stock</span>}
    </div>
  );
}

// TypeScript ensures correct props
<ProductCard
  name="Wireless Mouse"
  price={29.99}
  imageUrl="/images/mouse.jpg"
  inStock={true}
/>

// ❌ Error: missing 'inStock' property
<ProductCard
  name="Keyboard"
  price={99.99}
  imageUrl="/images/keyboard.jpg"
/>

Optional Props

interface ProductCardProps {
  name: string;
  price: number;
  imageUrl?: string;           // Optional
  inStock?: boolean;           // Optional with default
  discountPercent?: number;    // Optional
}

function ProductCard({
  name,
  price,
  imageUrl = "/images/placeholder.jpg",  // Default value
  inStock = true,                         // Default value
  discountPercent
}: ProductCardProps) {
  const finalPrice = discountPercent
    ? price * (1 - discountPercent / 100)
    : price;

  return (
    <div className="product-card">
      <img src={imageUrl} alt={name} />
      <h3>{name}</h3>
      {discountPercent && (
        <span className="discount">{discountPercent}% OFF</span>
      )}
      <p>${finalPrice.toFixed(2)}</p>
    </div>
  );
}

---

Typing Children

Basic Children

interface CardProps {
  title: string;
  children: React.ReactNode;  // Accepts anything renderable
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="Featured Products">
  <ProductCard {...product1} />
  <ProductCard {...product2} />
</Card>

Specific Children Types

interface ProductListProps {
  children: React.ReactElement<ProductCardProps>[];  // Only ProductCard allowed
}

// Or with PropsWithChildren helper
import { PropsWithChildren } from 'react';

interface CartSummaryProps {
  total: number;
}

function CartSummary({ total, children }: PropsWithChildren<CartSummaryProps>) {
  return (
    <div className="cart-summary">
      {children}
      <div className="total">Total: ${total.toFixed(2)}</div>
    </div>
  );
}

---

Typing useState

TypeScript infers state type from initial value:

function ShoppingCart() {
  // Inferred: number
  const [itemCount, setItemCount] = useState(0);

  // Inferred: string
  const [couponCode, setCouponCode] = useState("");

  // Inferred: boolean
  const [isCheckingOut, setIsCheckingOut] = useState(false);

  // Need explicit type for complex initial state
  const [cart, setCart] = useState<CartItem[]>([]);

  // Or for null initial values
  const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
}

useState with Interfaces

interface CartState {
  items: CartItem[];
  couponCode: string | null;
  isLoading: boolean;
}

function ShoppingCart() {
  const [cart, setCart] = useState<CartState>({
    items: [],
    couponCode: null,
    isLoading: false
  });

  const addItem = (product: Product, quantity: number) => {
    setCart(prev => ({
      ...prev,
      items: [...prev.items, { product, quantity }]
    }));
  };

  const applyCoupon = (code: string) => {
    setCart(prev => ({ ...prev, couponCode: code }));
  };

  return (/* ... */);
}

---

Typing Events

Click Events

interface AddToCartButtonProps {
  product: Product;
  onAddToCart: (product: Product) => void;
}

function AddToCartButton({ product, onAddToCart }: AddToCartButtonProps) {
  // Type the event handler
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    onAddToCart(product);
  };

  return (
    <button onClick={handleClick}>
      Add to Cart
    </button>
  );
}

Form Events

interface SearchBarProps {
  onSearch: (query: string) => void;
}

function SearchBar({ onSearch }: SearchBarProps) {
  const [query, setQuery] = useState("");

  // Input change event
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(event.target.value);
  };

  // Form submit event
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    onSearch(query);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search products..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

Select Events

interface CategoryFilterProps {
  categories: string[];
  selected: string;
  onChange: (category: string) => void;
}

function CategoryFilter({ categories, selected, onChange }: CategoryFilterProps) {
  const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    onChange(event.target.value);
  };

  return (
    <select value={selected} onChange={handleChange}>
      <option value="">All Categories</option>
      {categories.map(cat => (
        <option key={cat} value={cat}>{cat}</option>
      ))}
    </select>
  );
}

---

Typing useEffect

function ProductPage({ productId }: { productId: string }) {
  const [product, setProduct] = useState<Product | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // Async function inside useEffect
    const fetchProduct = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/products/${productId}`);
        const data: Product = await response.json();
        setProduct(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to load");
      } finally {
        setLoading(false);
      }
    };

    fetchProduct();
  }, [productId]);  // Dependency array

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!product) return <div>Product not found</div>;

  return <ProductCard {...product} />;
}

---

Callback Props Pattern

interface ProductListProps {
  products: Product[];
  onProductClick: (product: Product) => void;
  onAddToCart: (product: Product, quantity: number) => void;
}

function ProductList({ products, onProductClick, onAddToCart }: ProductListProps) {
  return (
    <div className="product-grid">
      {products.map(product => (
        <div key={product.id} onClick={() => onProductClick(product)}>
          <ProductCard {...product} />
          <button onClick={(e) => {
            e.stopPropagation();
            onAddToCart(product, 1);
          }}>
            Add to Cart
          </button>
        </div>
      ))}
    </div>
  );
}

// Parent component
function StorePage() {
  const [cart, setCart] = useState<CartItem[]>([]);

  const handleAddToCart = (product: Product, quantity: number) => {
    setCart(prev => [...prev, { product, quantity }]);
  };

  const handleProductClick = (product: Product) => {
    navigate(`/product/${product.id}`);
  };

  return (
    <ProductList
      products={products}
      onProductClick={handleProductClick}
      onAddToCart={handleAddToCart}
    />
  );
}

---

Generic Components

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage with products
<List
  items={products}
  renderItem={(product) => <ProductCard {...product} />}
  keyExtractor={(product) => product.id}
/>

// Usage with orders
<List
  items={orders}
  renderItem={(order) => <OrderSummary {...order} />}
  keyExtractor={(order) => order.id}
/>

---

Complete Example: Product Card Component

interface Product {
  id: string;
  name: string;
  price: number;
  imageUrl: string;
  inStock: boolean;
  rating: number;
}

interface ProductCardProps {
  product: Product;
  onAddToCart: (productId: string, quantity: number) => void;
  onWishlist?: (productId: string) => void;
}

function ProductCard({ product, onAddToCart, onWishlist }: ProductCardProps) {
  const [quantity, setQuantity] = useState(1);
  const [isAdding, setIsAdding] = useState(false);

  const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = parseInt(e.target.value, 10);
    if (value > 0) setQuantity(value);
  };

  const handleAddToCart = async () => {
    setIsAdding(true);
    await onAddToCart(product.id, quantity);
    setIsAdding(false);
    setQuantity(1);
  };

  return (
    <div className="product-card">
      <img src={product.imageUrl} alt={product.name} />

      <div className="product-info">
        <h3>{product.name}</h3>
        <div className="rating">{"⭐".repeat(Math.round(product.rating))}</div>
        <p className="price">${product.price.toFixed(2)}</p>
      </div>

      <div className="product-actions">
        <input
          type="number"
          min="1"
          value={quantity}
          onChange={handleQuantityChange}
          disabled={!product.inStock}
        />

        <button
          onClick={handleAddToCart}
          disabled={!product.inStock || isAdding}
        >
          {isAdding ? "Adding..." : product.inStock ? "Add to Cart" : "Out of Stock"}
        </button>

        {onWishlist && (
          <button onClick={() => onWishlist(product.id)}>
            ♡ Wishlist
          </button>
        )}
      </div>
    </div>
  );
}

---

Type Challenges

Challenge 1: Type a Quantity Selector

Complete the types for this component:

interface QuantitySelectorProps {
  // Your types here
}

function QuantitySelector({ value, onChange, min, max }: QuantitySelectorProps) {
  return (
    <div className="quantity-selector">
      <button onClick={() => onChange(value - 1)} disabled={value <= min}>-</button>
      <span>{value}</span>
      <button onClick={() => onChange(value + 1)} disabled={value >= max}>+</button>
    </div>
  );
}
Solution
interface QuantitySelectorProps {
  value: number;
  onChange: (newValue: number) => void;
  min?: number;
  max?: number;
}

function QuantitySelector({
  value,
  onChange,
  min = 1,
  max = 99
}: QuantitySelectorProps) {
  return (
    <div className="quantity-selector">
      <button onClick={() => onChange(value - 1)} disabled={value <= min}>-</button>
      <span>{value}</span>
      <button onClick={() => onChange(value + 1)} disabled={value >= max}>+</button>
    </div>
  );
}

---

Key Takeaways

ConceptSyntaxUse Case
Props interfaceinterface Props { }Define component props
ChildrenReact.ReactNodeAccept any renderable
useStateuseState(initial)Explicit state type
EventsReact.MouseEventType event handlers
Callbacks(item: T) => voidParent-child communication
Generic componentfunction Comp()Reusable typed components

You're now ready to build type-safe React applications!

Generics