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:
- Reusability 🔄: Write once, use everywhere
- Clean Components 🧹: Keep components focused on UI
- Type Safety 🔒: Catch errors at compile-time
- Better Testing 🧪: Test logic in isolation
- 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
- 🎯 Start with “use”: Always name hooks starting with “use”
- 📝 Define Clear Interfaces: Type what your hook returns
- 🔄 Use useCallback: Memoize functions to prevent unnecessary re-renders
- 💾 Use useMemo: Memoize expensive calculations
- ✨ Keep Hooks Simple: One responsibility per hook
- 🧪 Make Hooks Testable: Keep logic pure and testable
- 📖 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:
- 💻 Practice with the form validation exercise above
- 🏗️ Build your own custom hooks for common patterns you use
- 📚 Move on to our next tutorial: React Context with TypeScript
- 🌟 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! 🎣🚀✨