+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 158 of 355

🎣 Custom Hooks: Type-Safe Hook Creation

Master custom hooks: type-safe hook creation in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻
  • React fundamentals 🔄
  • TypeScript with React basics 📘

What you'll learn

  • Understand custom hooks fundamentals 🎯
  • Apply custom hooks in real projects 🏗️
  • Debug common hook issues 🐛
  • Write type-safe custom hooks ✨

🎯 Introduction

Welcome to this exciting tutorial on custom hooks! 🎉 In this guide, we’ll explore how to create your own reusable hooks that work seamlessly with TypeScript.

You’ll discover how custom hooks can transform your React development experience by encapsulating stateful logic and making it reusable across components. Whether you’re building data fetching utilities 🌐, form handlers 📝, or state management solutions 🗂️, understanding custom hooks is essential for writing clean, maintainable React code.

By the end of this tutorial, you’ll feel confident creating your own custom hooks and using them in real projects! Let’s dive in! 🏊‍♂️

📚 Understanding Custom Hooks

🤔 What are Custom Hooks?

Custom hooks are like building your own LEGO blocks 🧱 for React components. Think of them as reusable functions that contain stateful logic - you can pick them up and use them in any component that needs that functionality!

In TypeScript terms, custom hooks are functions that start with “use” and can call other hooks inside them ✨. This means you can:

  • 🔄 Reuse stateful logic across components
  • 🧹 Keep components clean and focused
  • 🚀 Build powerful abstractions
  • 🛡️ Get full type safety

💡 Why Use Custom Hooks?

Here’s why developers love custom hooks:

  1. Reusability 🔄: Write once, use everywhere
  2. Clean Components 🧹: Keep components focused on UI
  3. Type Safety 🔒: Catch errors at compile-time
  4. Better Testing 🧪: Test logic in isolation
  5. Code Organization 📁: Group related logic together

Real-world example: Imagine building a shopping app 🛒. With custom hooks, you can create a useCart hook that handles adding items, calculating totals, and managing cart state - then use it in any component that needs cart functionality!

🔧 Basic Syntax and Usage

📝 Simple Custom Hook Example

Let’s start with a friendly example:

// 🎣 Our first custom hook!
import { useState, useCallback } from 'react';

interface CounterHook {
  count: number;        // 📊 Current count value
  increment: () => void; // ➕ Function to increase count
  decrement: () => void; // ➖ Function to decrease count
  reset: () => void;     // 🔄 Function to reset count
}

const useCounter = (initialValue: number = 0): CounterHook => {
  const [count, setCount] = useState<number>(initialValue);
  
  // ➕ Increment function with useCallback for performance
  const increment = useCallback(() => {
    setCount(prev => prev + 1);
  }, []);
  
  // ➖ Decrement function
  const decrement = useCallback(() => {
    setCount(prev => prev - 1);
  }, []);
  
  // 🔄 Reset function
  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);
  
  return { count, increment, decrement, reset };
};

💡 Explanation: Notice how we define a clear interface for what the hook returns! This gives us perfect autocomplete and type safety.

🎯 Using Our Custom Hook

Here’s how to use it in a component:

// 🎮 Component using our custom hook
import React from 'react';

const CounterComponent: React.FC = () => {
  // 🎣 Using our custom hook - look how clean this is!
  const { count, increment, decrement, reset } = useCounter(10);
  
  return (
    <div className="p-4 bg-blue-50 rounded-lg">
      <h2 className="text-xl font-bold mb-4">🎮 Counter Game</h2>
      <div className="text-3xl font-bold text-center mb-4">
        {count} 🎯
      </div>
      <div className="flex gap-2 justify-center">
        <button onClick={increment} className="px-4 py-2 bg-green-500 text-white rounded">
Add
        </button>
        <button onClick={decrement} className="px-4 py-2 bg-red-500 text-white rounded">
Subtract
        </button>
        <button onClick={reset} className="px-4 py-2 bg-gray-500 text-white rounded">
          🔄 Reset
        </button>
      </div>
    </div>
  );
};

💡 Practical Examples

🛒 Example 1: Shopping Cart Hook

Let’s build something practical - a shopping cart hook:

// 🛍️ Define our types first
interface Product {
  id: string;
  name: string;
  price: number;
  emoji: string;
}

interface CartItem extends Product {
  quantity: number; // 📦 How many of this item
}

interface CartHook {
  items: CartItem[];
  total: number;
  itemCount: number;
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
}

// 🎣 Our shopping cart hook
const useShoppingCart = (): CartHook => {
  const [items, setItems] = useState<CartItem[]>([]);
  
  // ➕ Add item to cart
  const addItem = useCallback((product: Product) => {
    setItems(currentItems => {
      const existingItem = currentItems.find(item => item.id === product.id);
      
      if (existingItem) {
        // 📈 Item exists, increase quantity
        return currentItems.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        // ✨ New item, add to cart
        return [...currentItems, { ...product, quantity: 1 }];
      }
    });
  }, []);
  
  // ➖ Remove item completely
  const removeItem = useCallback((productId: string) => {
    setItems(currentItems => 
      currentItems.filter(item => item.id !== productId)
    );
  }, []);
  
  // 📝 Update quantity
  const updateQuantity = useCallback((productId: string, quantity: number) => {
    if (quantity <= 0) {
      removeItem(productId);
      return;
    }
    
    setItems(currentItems =>
      currentItems.map(item =>
        item.id === productId
          ? { ...item, quantity }
          : item
      )
    );
  }, [removeItem]);
  
  // 🧹 Clear entire cart
  const clearCart = useCallback(() => {
    setItems([]);
  }, []);
  
  // 💰 Calculate total price
  const total = useMemo(() =>
    items.reduce((sum, item) => sum + (item.price * item.quantity), 0),
    [items]
  );
  
  // 📊 Count total items
  const itemCount = useMemo(() =>
    items.reduce((count, item) => count + item.quantity, 0),
    [items]
  );
  
  return {
    items,
    total,
    itemCount,
    addItem,
    removeItem,
    updateQuantity,
    clearCart
  };
};

// 🎮 Using the cart hook in a component
const ShoppingApp: React.FC = () => {
  const cart = useShoppingCart();
  
  const sampleProducts: Product[] = [
    { id: '1', name: 'TypeScript Book', price: 29.99, emoji: '📘' },
    { id: '2', name: 'Coffee Mug', price: 12.99, emoji: '☕' },
    { id: '3', name: 'Laptop Sticker', price: 4.99, emoji: '💻' }
  ];
  
  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-6">🛒 TypeScript Shop</h1>
      
      {/* 🏪 Product List */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
        {sampleProducts.map(product => (
          <div key={product.id} className="p-4 border rounded-lg">
            <div className="text-4xl text-center mb-2">{product.emoji}</div>
            <h3 className="font-semibold text-center">{product.name}</h3>
            <p className="text-gray-600 text-center">${product.price}</p>
            <button
              onClick={() => cart.addItem(product)}
              className="w-full mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
            >
Add to Cart
            </button>
          </div>
        ))}
      </div>
      
      {/* 🛒 Cart Summary */}
      <div className="bg-gray-50 p-4 rounded-lg">
        <h2 className="text-xl font-bold mb-4">
          🛒 Cart ({cart.itemCount} items)
        </h2>
        
        {cart.items.length === 0 ? (
          <p className="text-gray-500">Your cart is empty 😢</p>
        ) : (
          <>
            {cart.items.map(item => (
              <div key={item.id} className="flex justify-between items-center mb-2">
                <span>{item.emoji} {item.name} × {item.quantity}</span>
                <span>${(item.price * item.quantity).toFixed(2)}</span>
              </div>
            ))}
            <div className="border-t pt-2 mt-4">
              <div className="flex justify-between items-center font-bold">
                <span>Total: ${cart.total.toFixed(2)} 💰</span>
                <button
                  onClick={cart.clearCart}
                  className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
                >
                  🧹 Clear Cart
                </button>
              </div>
            </div>
          </>
        )}
      </div>
    </div>
  );
};

🌐 Example 2: Data Fetching Hook

Let’s create a powerful data fetching hook:

// 📡 Generic data fetching hook
interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

interface FetchHook<T> extends FetchState<T> {
  refetch: () => void;
}

const useFetch = <T>(url: string): FetchHook<T> => {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null
  });
  
  // 🎣 Fetch function
  const fetchData = useCallback(async () => {
    setState(prev => ({ ...prev, loading: true, error: null }));
    
    try {
      const response = await fetch(url);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const data = await response.json();
      setState({ data, loading: false, error: null });
    } catch (err) {
      setState({
        data: null,
        loading: false,
        error: err instanceof Error ? err.message : 'Unknown error occurred'
      });
    }
  }, [url]);
  
  // 🚀 Fetch on mount and when URL changes
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  return {
    ...state,
    refetch: fetchData
  };
};

// 👤 User profile component using the fetch hook
interface User {
  id: number;
  name: string;
  email: string;
  emoji: string;
}

const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
  const { data: user, loading, error, refetch } = useFetch<User>(
    `/api/users/${userId}`
  );
  
  if (loading) return <div>🔄 Loading user profile...</div>;
  if (error) return <div>❌ Error: {error}</div>;
  if (!user) return <div>👤 No user found</div>;
  
  return (
    <div className="p-4 bg-white rounded-lg shadow">
      <div className="text-4xl text-center mb-4">{user.emoji}</div>
      <h2 className="text-xl font-bold text-center mb-2">{user.name}</h2>
      <p className="text-gray-600 text-center mb-4">{user.email}</p>
      <button
        onClick={refetch}
        className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        🔄 Refresh Profile
      </button>
    </div>
  );
};

🚀 Advanced Concepts

🧙‍♂️ Advanced Topic 1: Generic Custom Hooks

When you’re ready to level up, try creating generic hooks:

// 🎯 Generic local storage hook
const useLocalStorage = <T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] => {
  // 📖 Get initial value from localStorage or use provided initial value
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });
  
  // 💾 Save to localStorage whenever value changes
  const setValue = useCallback((value: T | ((prev: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);
  
  return [storedValue, setValue];
};

// 🎮 Using the generic localStorage hook
const SettingsComponent: React.FC = () => {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
  const [username, setUsername] = useLocalStorage<string>('username', '');
  
  return (
    <div className="p-4">
      <h2 className="text-xl font-bold mb-4">⚙️ Settings</h2>
      
      <div className="mb-4">
        <label className="block mb-2">🎨 Theme:</label>
        <select
          value={theme}
          onChange={(e) => setTheme(e.target.value as 'light' | 'dark')}
          className="p-2 border rounded"
        >
          <option value="light">☀️ Light</option>
          <option value="dark">🌙 Dark</option>
        </select>
      </div>
      
      <div className="mb-4">
        <label className="block mb-2">👤 Username:</label>
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          className="p-2 border rounded w-full"
          placeholder="Enter your username"
        />
      </div>
      
      <p className="text-sm text-gray-600">
Settings are automatically saved to localStorage!
      </p>
    </div>
  );
};

🏗️ Advanced Topic 2: Hooks with Complex State

For the brave developers, here’s a hook with complex state management:

// 🎮 Game state management hook
interface GameState {
  score: number;
  level: number;
  lives: number;
  powerUps: string[];
  gameStatus: 'playing' | 'paused' | 'gameOver' | 'victory';
}

type GameAction =
  | { type: 'ADD_SCORE'; points: number }
  | { type: 'LEVEL_UP' }
  | { type: 'LOSE_LIFE' }
  | { type: 'ADD_POWER_UP'; powerUp: string }
  | { type: 'USE_POWER_UP'; powerUp: string }
  | { type: 'PAUSE_GAME' }
  | { type: 'RESUME_GAME' }
  | { type: 'RESET_GAME' };

// 🔄 Game reducer
const gameReducer = (state: GameState, action: GameAction): GameState => {
  switch (action.type) {
    case 'ADD_SCORE':
      return {
        ...state,
        score: state.score + action.points
      };
    
    case 'LEVEL_UP':
      return {
        ...state,
        level: state.level + 1,
        score: state.score + 100 // 🎉 Bonus points for leveling up!
      };
    
    case 'LOSE_LIFE':
      const newLives = state.lives - 1;
      return {
        ...state,
        lives: newLives,
        gameStatus: newLives <= 0 ? 'gameOver' : state.gameStatus
      };
    
    case 'ADD_POWER_UP':
      return {
        ...state,
        powerUps: [...state.powerUps, action.powerUp]
      };
    
    case 'USE_POWER_UP':
      return {
        ...state,
        powerUps: state.powerUps.filter(p => p !== action.powerUp)
      };
    
    case 'PAUSE_GAME':
      return { ...state, gameStatus: 'paused' };
    
    case 'RESUME_GAME':
      return { ...state, gameStatus: 'playing' };
    
    case 'RESET_GAME':
      return {
        score: 0,
        level: 1,
        lives: 3,
        powerUps: [],
        gameStatus: 'playing'
      };
    
    default:
      return state;
  }
};

// 🎣 Game hook
const useGame = () => {
  const [state, dispatch] = useReducer(gameReducer, {
    score: 0,
    level: 1,
    lives: 3,
    powerUps: [],
    gameStatus: 'playing'
  });
  
  // 🎯 Action creators
  const addScore = useCallback((points: number) => {
    dispatch({ type: 'ADD_SCORE', points });
  }, []);
  
  const levelUp = useCallback(() => {
    dispatch({ type: 'LEVEL_UP' });
  }, []);
  
  const loseLife = useCallback(() => {
    dispatch({ type: 'LOSE_LIFE' });
  }, []);
  
  const addPowerUp = useCallback((powerUp: string) => {
    dispatch({ type: 'ADD_POWER_UP', powerUp });
  }, []);
  
  const usePowerUp = useCallback((powerUp: string) => {
    dispatch({ type: 'USE_POWER_UP', powerUp });
  }, []);
  
  const pauseGame = useCallback(() => {
    dispatch({ type: 'PAUSE_GAME' });
  }, []);
  
  const resumeGame = useCallback(() => {
    dispatch({ type: 'RESUME_GAME' });
  }, []);
  
  const resetGame = useCallback(() => {
    dispatch({ type: 'RESET_GAME' });
  }, []);
  
  return {
    ...state,
    addScore,
    levelUp,
    loseLife,
    addPowerUp,
    usePowerUp,
    pauseGame,
    resumeGame,
    resetGame
  };
};

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Forgetting useCallback

// ❌ Wrong way - creates new function on every render!
const useBadHook = () => {
  const [count, setCount] = useState(0);
  
  const increment = () => {  // 💥 New function every render!
    setCount(prev => prev + 1);
  };
  
  return { count, increment };
};

// ✅ Correct way - memoize with useCallback!
const useGoodHook = () => {
  const [count, setCount] = useState(0);
  
  const increment = useCallback(() => {  // ✨ Memoized function!
    setCount(prev => prev + 1);
  }, []);
  
  return { count, increment };
};

🤯 Pitfall 2: Missing Dependencies

// ❌ Dangerous - missing dependency!
const useBadEffect = (userId: string) => {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);  // 💥 userId not in deps!
  }, []); // Missing userId dependency
  
  return user;
};

// ✅ Safe - include all dependencies!
const useGoodEffect = (userId: string) => {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(setUser);  // ✅ Proper dependency!
  }, [userId]); // Include userId in dependencies
  
  return user;
};

🚫 Pitfall 3: Not Starting with “use”

// ❌ Wrong - doesn't follow hook naming convention!
const counterLogic = () => {  // 💥 React won't recognize this as a hook!
  const [count, setCount] = useState(0);
  return { count, setCount };
};

// ✅ Correct - starts with "use"!
const useCounter = () => {  // ✨ Proper hook naming!
  const [count, setCount] = useState(0);
  return { count, setCount };
};

🛠️ Best Practices

  1. 🎯 Start with “use”: Always name hooks starting with “use”
  2. 📝 Define Clear Interfaces: Type what your hook returns
  3. 🔄 Use useCallback: Memoize functions to prevent unnecessary re-renders
  4. 💾 Use useMemo: Memoize expensive calculations
  5. ✨ Keep Hooks Simple: One responsibility per hook
  6. 🧪 Make Hooks Testable: Keep logic pure and testable
  7. 📖 Document Your Hooks: Clear names and TypeScript types serve as documentation

🧪 Hands-On Exercise

🎯 Challenge: Build a Form Validation Hook

Create a comprehensive form validation hook:

📋 Requirements:

  • ✅ Handle multiple form fields
  • 🔍 Real-time validation
  • 🎯 Custom validation rules
  • 📊 Track field touched state
  • 🚀 Form submission handling
  • 🎨 TypeScript support with generics

🚀 Bonus Points:

  • Add async validation (email uniqueness check)
  • Include debounced validation
  • Add form reset functionality
  • Create validation rule presets

💡 Solution

🔍 Click to see solution
// 🎯 Our type-safe form validation hook!
interface ValidationRule<T> {
  validate: (value: T) => boolean;
  message: string;
}

interface FieldConfig<T> {
  initialValue: T;
  rules?: ValidationRule<T>[];
  required?: boolean;
}

interface FieldState<T> {
  value: T;
  error: string | null;
  touched: boolean;
}

interface FormState<T extends Record<string, any>> {
  [K in keyof T]: FieldState<T[K]>;
}

interface FormHook<T extends Record<string, any>> {
  values: { [K in keyof T]: T[K] };
  errors: { [K in keyof T]: string | null };
  touched: { [K in keyof T]: boolean };
  isValid: boolean;
  handleChange: <K extends keyof T>(field: K, value: T[K]) => void;
  handleBlur: <K extends keyof T>(field: K) => void;
  handleSubmit: (onSubmit: (values: T) => void) => (e: React.FormEvent) => void;
  reset: () => void;
  setFieldError: <K extends keyof T>(field: K, error: string) => void;
}

// 🎣 The magical form validation hook
const useForm = <T extends Record<string, any>>(
  config: { [K in keyof T]: FieldConfig<T[K]> }
): FormHook<T> => {
  // 🏗️ Initialize form state
  const initialState = useMemo(() => {
    const state = {} as FormState<T>;
    Object.entries(config).forEach(([key, fieldConfig]) => {
      state[key as keyof T] = {
        value: (fieldConfig as FieldConfig<any>).initialValue,
        error: null,
        touched: false
      };
    });
    return state;
  }, [config]);
  
  const [formState, setFormState] = useState<FormState<T>>(initialState);
  
  // 🔍 Validate a single field
  const validateField = useCallback(<K extends keyof T>(
    field: K,
    value: T[K]
  ): string | null => {
    const fieldConfig = config[field];
    
    // ✅ Check required
    if (fieldConfig.required && (!value || value === '')) {
      return `${String(field)} is required`;
    }
    
    // 🎯 Run custom validation rules
    if (fieldConfig.rules) {
      for (const rule of fieldConfig.rules) {
        if (!rule.validate(value)) {
          return rule.message;
        }
      }
    }
    
    return null;
  }, [config]);
  
  // 📝 Handle field change
  const handleChange = useCallback(<K extends keyof T>(
    field: K,
    value: T[K]
  ) => {
    setFormState(prev => ({
      ...prev,
      [field]: {
        ...prev[field],
        value,
        error: validateField(field, value)
      }
    }));
  }, [validateField]);
  
  // 👆 Handle field blur
  const handleBlur = useCallback(<K extends keyof T>(field: K) => {
    setFormState(prev => ({
      ...prev,
      [field]: {
        ...prev[field],
        touched: true
      }
    }));
  }, []);
  
  // 📊 Set field error manually
  const setFieldError = useCallback(<K extends keyof T>(
    field: K,
    error: string
  ) => {
    setFormState(prev => ({
      ...prev,
      [field]: {
        ...prev[field],
        error
      }
    }));
  }, []);
  
  // 🔄 Reset form
  const reset = useCallback(() => {
    setFormState(initialState);
  }, [initialState]);
  
  // 🚀 Handle form submission
  const handleSubmit = useCallback((
    onSubmit: (values: T) => void
  ) => {
    return (e: React.FormEvent) => {
      e.preventDefault();
      
      // 🔍 Validate all fields
      let hasErrors = false;
      const newFormState = { ...formState };
      
      Object.keys(config).forEach(key => {
        const field = key as keyof T;
        const error = validateField(field, formState[field].value);
        newFormState[field] = {
          ...newFormState[field],
          error,
          touched: true
        };
        if (error) hasErrors = true;
      });
      
      setFormState(newFormState);
      
      // ✅ Submit if no errors
      if (!hasErrors) {
        const values = {} as T;
        Object.keys(formState).forEach(key => {
          const field = key as keyof T;
          values[field] = formState[field].value;
        });
        onSubmit(values);
      }
    };
  }, [formState, config, validateField]);
  
  // 📊 Computed values
  const values = useMemo(() => {
    const vals = {} as { [K in keyof T]: T[K] };
    Object.keys(formState).forEach(key => {
      const field = key as keyof T;
      vals[field] = formState[field].value;
    });
    return vals;
  }, [formState]);
  
  const errors = useMemo(() => {
    const errs = {} as { [K in keyof T]: string | null };
    Object.keys(formState).forEach(key => {
      const field = key as keyof T;
      errs[field] = formState[field].error;
    });
    return errs;
  }, [formState]);
  
  const touched = useMemo(() => {
    const touchedFields = {} as { [K in keyof T]: boolean };
    Object.keys(formState).forEach(key => {
      const field = key as keyof T;
      touchedFields[field] = formState[field].touched;
    });
    return touchedFields;
  }, [formState]);
  
  const isValid = useMemo(() => {
    return Object.values(formState).every(field => !field.error);
  }, [formState]);
  
  return {
    values,
    errors,
    touched,
    isValid,
    handleChange,
    handleBlur,
    handleSubmit,
    reset,
    setFieldError
  };
};

// 🎮 Example usage - Registration Form
interface RegistrationForm {
  email: string;
  password: string;
  confirmPassword: string;
  username: string;
}

const RegistrationComponent: React.FC = () => {
  const form = useForm<RegistrationForm>({
    email: {
      initialValue: '',
      required: true,
      rules: [
        {
          validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
          message: 'Please enter a valid email address'
        }
      ]
    },
    password: {
      initialValue: '',
      required: true,
      rules: [
        {
          validate: (value) => value.length >= 8,
          message: 'Password must be at least 8 characters'
        }
      ]
    },
    confirmPassword: {
      initialValue: '',
      required: true,
      rules: [
        {
          validate: (value) => value === form.values.password,
          message: 'Passwords do not match'
        }
      ]
    },
    username: {
      initialValue: '',
      required: true,
      rules: [
        {
          validate: (value) => value.length >= 3,
          message: 'Username must be at least 3 characters'
        }
      ]
    }
  });
  
  const handleRegistration = (values: RegistrationForm) => {
    console.log('🎉 Registration successful!', values);
    // Here you would send data to your API
  };
  
  return (
    <form onSubmit={form.handleSubmit(handleRegistration)} className="max-w-md mx-auto p-6">
      <h2 className="text-2xl font-bold mb-6">🚀 Create Account</h2>
      
      {/* 📧 Email Field */}
      <div className="mb-4">
        <label className="block mb-2 font-semibold">📧 Email:</label>
        <input
          type="email"
          value={form.values.email}
          onChange={(e) => form.handleChange('email', e.target.value)}
          onBlur={() => form.handleBlur('email')}
          className={`w-full p-2 border rounded ${
            form.touched.email && form.errors.email ? 'border-red-500' : 'border-gray-300'
          }`}
          placeholder="[email protected]"
        />
        {form.touched.email && form.errors.email && (
          <p className="text-red-500 text-sm mt-1">❌ {form.errors.email}</p>
        )}
      </div>
      
      {/* 🔒 Password Field */}
      <div className="mb-4">
        <label className="block mb-2 font-semibold">🔒 Password:</label>
        <input
          type="password"
          value={form.values.password}
          onChange={(e) => form.handleChange('password', e.target.value)}
          onBlur={() => form.handleBlur('password')}
          className={`w-full p-2 border rounded ${
            form.touched.password && form.errors.password ? 'border-red-500' : 'border-gray-300'
          }`}
          placeholder="Enter secure password"
        />
        {form.touched.password && form.errors.password && (
          <p className="text-red-500 text-sm mt-1">❌ {form.errors.password}</p>
        )}
      </div>
      
      {/* 🔐 Confirm Password Field */}
      <div className="mb-4">
        <label className="block mb-2 font-semibold">🔐 Confirm Password:</label>
        <input
          type="password"
          value={form.values.confirmPassword}
          onChange={(e) => form.handleChange('confirmPassword', e.target.value)}
          onBlur={() => form.handleBlur('confirmPassword')}
          className={`w-full p-2 border rounded ${
            form.touched.confirmPassword && form.errors.confirmPassword ? 'border-red-500' : 'border-gray-300'
          }`}
          placeholder="Confirm your password"
        />
        {form.touched.confirmPassword && form.errors.confirmPassword && (
          <p className="text-red-500 text-sm mt-1">❌ {form.errors.confirmPassword}</p>
        )}
      </div>
      
      {/* 👤 Username Field */}
      <div className="mb-6">
        <label className="block mb-2 font-semibold">👤 Username:</label>
        <input
          type="text"
          value={form.values.username}
          onChange={(e) => form.handleChange('username', e.target.value)}
          onBlur={() => form.handleBlur('username')}
          className={`w-full p-2 border rounded ${
            form.touched.username && form.errors.username ? 'border-red-500' : 'border-gray-300'
          }`}
          placeholder="Choose a username"
        />
        {form.touched.username && form.errors.username && (
          <p className="text-red-500 text-sm mt-1">❌ {form.errors.username}</p>
        )}
      </div>
      
      {/* 🚀 Submit Button */}
      <button
        type="submit"
        disabled={!form.isValid}
        className={`w-full py-2 px-4 rounded font-semibold ${
          form.isValid
            ? 'bg-green-500 hover:bg-green-600 text-white'
            : 'bg-gray-300 text-gray-500 cursor-not-allowed'
        }`}
      >
        {form.isValid ? '🚀 Create Account' : '⏳ Fill out all fields'}
      </button>
      
      <div className="mt-4 text-center">
        <button
          type="button"
          onClick={form.reset}
          className="text-blue-500 hover:text-blue-700 underline"
        >
          🔄 Reset Form
        </button>
      </div>
    </form>
  );
};

🎓 Key Takeaways

You’ve learned so much! Here’s what you can now do:

  • Create custom hooks with confidence 💪
  • Use TypeScript types for perfect type safety 🛡️
  • Apply performance optimizations with useCallback and useMemo 🚀
  • Build reusable logic that works across components 🔄
  • Debug hook issues like a pro 🐛
  • Follow best practices for maintainable code ✨

Remember: Custom hooks are your secret weapon for creating clean, reusable React components! They help you extract complex logic and make it available wherever you need it. 🤝

🤝 Next Steps

Congratulations! 🎉 You’ve mastered custom hooks in TypeScript!

Here’s what to do next:

  1. 💻 Practice with the form validation exercise above
  2. 🏗️ Build your own custom hooks for common patterns you use
  3. 📚 Move on to our next tutorial: React Context with TypeScript
  4. 🌟 Share your custom hooks with the community!

Remember: Every React expert started with simple hooks. Keep building, keep learning, and most importantly, have fun creating reusable magic! 🚀


Happy hooking! 🎣🚀✨