Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
What you'll learn
- Understand the concept fundamentals 🎯
- Apply the concept in real projects 🏗️
- Debug common issues 🐛
- Write type-safe code ✨
🎯 Introduction
Welcome to this exciting tutorial on React Hooks with TypeScript! 🎉 In this guide, we’ll explore how TypeScript transforms your React development experience by adding type safety to hooks like useState, useEffect, and custom hooks.
You’ll discover how TypeScript can catch bugs before they happen, provide better autocomplete, and make your React code more maintainable. Whether you’re building todo apps 📋, shopping carts 🛒, or complex dashboards 📊, understanding typed hooks is essential for modern React development.
By the end of this tutorial, you’ll feel confident writing type-safe React components with hooks! Let’s dive in! 🏊♂️
📚 Understanding React Hooks with TypeScript
🤔 What are Typed React Hooks?
React Hooks with TypeScript are like having a smart assistant 🤖 that helps you manage component state and side effects while preventing common mistakes. Think of it as adding guardrails 🛡️ to your React development highway - you can still drive fast, but you’re protected from dangerous mistakes!
In TypeScript terms, hooks become type-safe functions that help you:
- ✨ Catch state type errors at compile-time
- 🚀 Get better IDE autocomplete and IntelliSense
- 🛡️ Prevent runtime errors from incorrect data types
- 📖 Self-document your component’s state structure
💡 Why Use TypeScript with React Hooks?
Here’s why developers love this combination:
- Type Safety 🔒: Catch state errors before they reach production
- Better IDE Support 💻: Amazing autocomplete for state and props
- Refactoring Confidence 🔧: Change state structure without fear
- Self-Documenting Code 📖: State types serve as inline documentation
Real-world example: Imagine building a user profile form 👤. With TypeScript, you can define exactly what data your state should hold, and TypeScript will prevent you from accidentally setting a string where you need a number!
🔧 Basic Syntax and Usage
📝 useState with TypeScript
Let’s start with the most common hook:
import React, { useState } from 'react';
// 🎯 Simple string state - TypeScript infers the type!
const [name, setName] = useState<string>('');
// 🔢 Number state with explicit typing
const [count, setCount] = useState<number>(0);
// 🎭 Boolean state
const [isVisible, setIsVisible] = useState<boolean>(false);
// 🎨 Object state with interface
interface User {
id: number;
name: string;
email: string;
avatar?: string; // 📸 Optional profile picture
}
const [user, setUser] = useState<User | null>(null);
💡 Pro Tip: TypeScript can often infer simple types, but being explicit helps with complex types and makes your code more readable!
🔄 useEffect with TypeScript
TypeScript makes useEffect safer by ensuring your dependencies are correctly typed:
import React, { useState, useEffect } from 'react';
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
// 📡 Fetch user data on mount
useEffect(() => {
const fetchUser = async (): Promise<void> => {
try {
setLoading(true);
// 🌐 API call would go here
const userData: User = {
id: 1,
name: "Sarah Developer",
email: "[email protected]",
avatar: "👩💻"
};
setUser(userData);
} catch (error) {
console.error('💥 Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, []); // 🎯 Empty dependency array - runs once
// 📱 Update document title when user changes
useEffect(() => {
if (user) {
document.title = `Profile: ${user.name} 👤`;
}
}, [user]); // 🔄 Runs when user changes
return (
<div>
{loading ? (
<p>⏳ Loading...</p>
) : (
<div>
<h1>{user?.avatar} {user?.name}</h1>
<p>📧 {user?.email}</p>
</div>
)}
</div>
);
};
💡 Practical Examples
🛒 Example 1: Shopping Cart with TypeScript Hooks
Let’s build a type-safe shopping cart:
import React, { useState, useCallback } from 'react';
// 🛍️ Product interface
interface Product {
id: string;
name: string;
price: number;
emoji: string;
category: 'electronics' | 'clothing' | 'books';
}
// 🛒 Cart item with quantity
interface CartItem extends Product {
quantity: number;
}
const ShoppingCart: React.FC = () => {
const [cart, setCart] = useState<CartItem[]>([]);
const [total, setTotal] = useState<number>(0);
// ➕ Add item to cart with type safety
const addToCart = useCallback((product: Product): void => {
setCart(prevCart => {
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
// 📈 Increase quantity
return prevCart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
// 🆕 Add new item
return [...prevCart, { ...product, quantity: 1 }];
}
});
}, []);
// ➖ Remove item from cart
const removeFromCart = useCallback((productId: string): void => {
setCart(prevCart => prevCart.filter(item => item.id !== productId));
}, []);
// 💰 Calculate total whenever cart changes
React.useEffect(() => {
const newTotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
setTotal(newTotal);
}, [cart]);
return (
<div className="p-4">
<h2>🛒 Your Shopping Cart</h2>
{cart.length === 0 ? (
<p>🛍️ Your cart is empty</p>
) : (
<>
{cart.map(item => (
<div key={item.id} className="flex justify-between items-center p-2 border-b">
<span>{item.emoji} {item.name} x{item.quantity}</span>
<div>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button
onClick={() => removeFromCart(item.id)}
className="ml-2 text-red-500"
>
🗑️
</button>
</div>
</div>
))}
<div className="mt-4 text-xl font-bold">
💰 Total: ${total.toFixed(2)}
</div>
</>
)}
</div>
);
};
🎮 Example 2: Game Score Tracker with Custom Hook
Let’s create a custom hook for game state management:
import React, { useState, useEffect, useCallback } from 'react';
// 🎯 Game score interface
interface GameScore {
player: string;
score: number;
level: number;
lives: number;
achievements: string[];
}
// 🎮 Custom hook for game management
const useGameState = (playerName: string) => {
const [gameState, setGameState] = useState<GameScore>({
player: playerName,
score: 0,
level: 1,
lives: 3,
achievements: ['🌟 First Steps']
});
const [isGameOver, setIsGameOver] = useState<boolean>(false);
// 🎯 Add points with type safety
const addPoints = useCallback((points: number): void => {
if (isGameOver) return;
setGameState(prev => {
const newScore = prev.score + points;
const newLevel = Math.floor(newScore / 1000) + 1;
const achievements = [...prev.achievements];
// 🏆 Level up achievements
if (newLevel > prev.level) {
achievements.push(`🏆 Level ${newLevel} Master`);
}
// 🎊 Score milestones
if (newScore >= 5000 && !achievements.includes('💎 High Scorer')) {
achievements.push('💎 High Scorer');
}
return {
...prev,
score: newScore,
level: newLevel,
achievements
};
});
}, [isGameOver]);
// 💔 Lose a life
const loseLife = useCallback((): void => {
setGameState(prev => {
const newLives = prev.lives - 1;
if (newLives <= 0) {
setIsGameOver(true);
}
return { ...prev, lives: newLives };
});
}, []);
// 🔄 Reset game
const resetGame = useCallback((): void => {
setGameState({
player: playerName,
score: 0,
level: 1,
lives: 3,
achievements: ['🌟 First Steps']
});
setIsGameOver(false);
}, [playerName]);
// 📊 Game stats
useEffect(() => {
if (gameState.score > 0) {
console.log(`🎮 ${gameState.player} scored ${gameState.score} points!`);
}
}, [gameState.score, gameState.player]);
return {
gameState,
isGameOver,
addPoints,
loseLife,
resetGame
};
};
// 🎮 Game component using our custom hook
const GameDashboard: React.FC<{ playerName: string }> = ({ playerName }) => {
const { gameState, isGameOver, addPoints, loseLife, resetGame } = useGameState(playerName);
return (
<div className="p-6 bg-gray-100 rounded-lg">
<h2 className="text-2xl font-bold mb-4">🎮 Game Dashboard</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<div>👤 Player: {gameState.player}</div>
<div>🎯 Score: {gameState.score}</div>
<div>📈 Level: {gameState.level}</div>
<div>❤️ Lives: {gameState.lives}</div>
</div>
<div className="mb-4">
<h3 className="font-semibold">🏆 Achievements:</h3>
{gameState.achievements.map((achievement, index) => (
<span key={index} className="mr-2">{achievement}</span>
))}
</div>
{isGameOver ? (
<div className="text-center">
<h3 className="text-xl mb-4">💀 Game Over!</h3>
<button
onClick={resetGame}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
🔄 Play Again
</button>
</div>
) : (
<div className="flex gap-2">
<button
onClick={() => addPoints(100)}
className="bg-green-500 text-white px-4 py-2 rounded"
>
➕ Add Points
</button>
<button
onClick={loseLife}
className="bg-red-500 text-white px-4 py-2 rounded"
>
💔 Lose Life
</button>
</div>
)}
</div>
);
};
🚀 Advanced Concepts
🧙♂️ Advanced Hook Types: useReducer with TypeScript
When state gets complex, useReducer with TypeScript is magical:
import React, { useReducer } from 'react';
// 🎯 State interface
interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
isLoading: boolean;
}
// 🎬 Action types
type TodoAction =
| { type: 'ADD_TODO'; payload: { text: string; emoji: string } }
| { type: 'TOGGLE_TODO'; payload: { id: string } }
| { type: 'DELETE_TODO'; payload: { id: string } }
| { type: 'SET_FILTER'; payload: { filter: TodoState['filter'] } }
| { type: 'SET_LOADING'; payload: { isLoading: boolean } };
// 🏭 Reducer function with full type safety
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now().toString(),
text: action.payload.text,
emoji: action.payload.emoji,
completed: false,
createdAt: new Date()
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_FILTER':
return { ...state, filter: action.payload.filter };
default:
return state;
}
};
🏗️ Advanced Custom Hooks with Generics
For the brave developers, here’s a reusable data fetching hook:
import { useState, useEffect } from 'react';
// 🚀 Generic data fetching hook
function useApi<T>(url: string): {
data: T | null;
loading: boolean;
error: string | null;
} {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async (): Promise<void> => {
try {
setLoading(true);
setError(null);
// 🌐 Simulate API call
const response = await fetch(url);
const result: T = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong');
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// 🎮 Usage with specific types
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC = () => {
const { data: user, loading, error } = useApi<User>('/api/user/1');
if (loading) return <div>⏳ Loading...</div>;
if (error) return <div>💥 Error: {error}</div>;
return (
<div>
<h1>👤 {user?.name}</h1>
<p>📧 {user?.email}</p>
</div>
);
};
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Not Typing useState Properly
// ❌ Wrong way - loses type information!
const [user, setUser] = useState(null); // TypeScript thinks it's always null!
// ✅ Correct way - explicit union type!
const [user, setUser] = useState<User | null>(null);
// ✅ Alternative - with initial value
const [user, setUser] = useState<User>({
id: 0,
name: '',
email: ''
});
🤯 Pitfall 2: Missing Dependencies in useEffect
// ❌ Dangerous - missing dependency!
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
useEffect(() => {
const result = count * multiplier; // 💥 Stale closure!
console.log(result);
}, [count]); // 🚫 Missing multiplier!
// ✅ Safe - include all dependencies!
useEffect(() => {
const result = count * multiplier; // ✅ Always fresh values!
console.log(result);
}, [count, multiplier]); // ✅ All dependencies included!
🔥 Pitfall 3: Incorrect Event Handler Types
// ❌ Wrong way - any type loses safety!
const handleChange = (event: any) => {
setValue(event.target.value); // 💥 Could be undefined!
};
// ✅ Correct way - specific event type!
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value); // ✅ TypeScript knows this exists!
};
🛠️ Best Practices
- 🎯 Be Explicit with Complex Types: Don’t rely on inference for complex state
- 📝 Use Interfaces for State Objects: Keep your state structure clear
- 🔒 Enable Strict Mode: Turn on all TypeScript safety features
- 🎨 Create Custom Hooks: Extract reusable stateful logic
- ✨ Use useCallback and useMemo: Optimize performance with proper typing
- 🛡️ Handle Loading and Error States: Always account for async operations
🧪 Hands-On Exercise
🎯 Challenge: Build a Type-Safe Todo App with Hooks
Create a complete todo application with TypeScript and React hooks:
📋 Requirements:
- ✅ Add, toggle, and delete todos
- 🏷️ Filter todos (all, active, completed)
- 💾 Persist todos to localStorage
- 📊 Show completion statistics
- 🎨 Each todo needs an emoji and priority level
- ⏰ Add due dates with reminders
🚀 Bonus Points:
- Use useReducer for complex state management
- Create custom hooks for localStorage and filtering
- Add drag-and-drop reordering with proper types
- Implement undo/redo functionality
💡 Solution
🔍 Click to see solution
import React, { useState, useEffect, useCallback, useReducer } from 'react';
// 🗂️ Todo interface
interface Todo {
id: string;
text: string;
emoji: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
createdAt: Date;
dueDate?: Date;
}
// 📊 App state
interface AppState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
stats: {
total: number;
completed: number;
percentage: number;
};
}
// 🎬 Actions
type AppAction =
| { type: 'ADD_TODO'; payload: Omit<Todo, 'id' | 'createdAt'> }
| { type: 'TOGGLE_TODO'; payload: { id: string } }
| { type: 'DELETE_TODO'; payload: { id: string } }
| { type: 'SET_FILTER'; payload: { filter: AppState['filter'] } }
| { type: 'LOAD_TODOS'; payload: { todos: Todo[] } };
// 🏭 Reducer
const appReducer = (state: AppState, action: AppAction): AppState => {
let newTodos: Todo[];
switch (action.type) {
case 'ADD_TODO':
newTodos = [...state.todos, {
...action.payload,
id: Date.now().toString(),
createdAt: new Date()
}];
break;
case 'TOGGLE_TODO':
newTodos = state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
);
break;
case 'DELETE_TODO':
newTodos = state.todos.filter(todo => todo.id !== action.payload.id);
break;
case 'SET_FILTER':
return { ...state, filter: action.payload.filter };
case 'LOAD_TODOS':
newTodos = action.payload.todos;
break;
default:
return state;
}
// 📊 Calculate stats
const completed = newTodos.filter(todo => todo.completed).length;
const total = newTodos.length;
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
return {
...state,
todos: newTodos,
stats: { total, completed, percentage }
};
};
// 💾 Custom hook for localStorage
const useLocalStorage = <T>(key: string, initialValue: T): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`💥 Error loading ${key} from localStorage:`, error);
return initialValue;
}
});
const setValue = (value: T): void => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`💥 Error saving ${key} to localStorage:`, error);
}
};
return [storedValue, setValue];
};
// 🏠 Main App Component
const TodoApp: React.FC = () => {
const [state, dispatch] = useReducer(appReducer, {
todos: [],
filter: 'all',
stats: { total: 0, completed: 0, percentage: 0 }
});
const [, saveToStorage] = useLocalStorage('todos', []);
// 💾 Save to localStorage when todos change
useEffect(() => {
saveToStorage(state.todos);
}, [state.todos, saveToStorage]);
// 📝 Add new todo
const addTodo = useCallback((text: string, emoji: string, priority: Todo['priority']) => {
if (text.trim()) {
dispatch({
type: 'ADD_TODO',
payload: { text: text.trim(), emoji, priority, completed: false }
});
}
}, []);
// 🔄 Toggle todo completion
const toggleTodo = useCallback((id: string) => {
dispatch({ type: 'TOGGLE_TODO', payload: { id } });
}, []);
// 🗑️ Delete todo
const deleteTodo = useCallback((id: string) => {
dispatch({ type: 'DELETE_TODO', payload: { id } });
}, []);
// 🏷️ Filter todos
const filteredTodos = state.todos.filter(todo => {
switch (state.filter) {
case 'active': return !todo.completed;
case 'completed': return todo.completed;
default: return true;
}
});
return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-lg">
<h1 className="text-3xl font-bold text-center mb-6">📝 TypeScript Todos</h1>
{/* 📊 Stats */}
<div className="mb-4 p-3 bg-gray-100 rounded">
<div>📊 Progress: {state.stats.completed}/{state.stats.total} ({state.stats.percentage}%)</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${state.stats.percentage}%` }}
></div>
</div>
</div>
{/* 🎯 Filter buttons */}
<div className="flex gap-2 mb-4">
{(['all', 'active', 'completed'] as const).map(filter => (
<button
key={filter}
onClick={() => dispatch({ type: 'SET_FILTER', payload: { filter } })}
className={`px-3 py-1 rounded ${
state.filter === filter
? 'bg-blue-500 text-white'
: 'bg-gray-200'
}`}
>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</button>
))}
</div>
{/* 📋 Todo list */}
<div className="space-y-2">
{filteredTodos.map(todo => (
<div key={todo.id} className="flex items-center gap-3 p-2 border rounded">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="mr-2"
/>
<span className={todo.completed ? 'line-through text-gray-500' : ''}>
{todo.emoji} {todo.text}
</span>
<span className={`ml-auto px-2 py-1 text-xs rounded ${
todo.priority === 'high' ? 'bg-red-100 text-red-800' :
todo.priority === 'medium' ? 'bg-yellow-100 text-yellow-800' :
'bg-green-100 text-green-800'
}`}>
{todo.priority}
</span>
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700"
>
🗑️
</button>
</div>
))}
</div>
{filteredTodos.length === 0 && (
<p className="text-center text-gray-500 mt-4">
{state.filter === 'all' ? '📝 No todos yet!' :
state.filter === 'active' ? '✅ All done!' :
'🎯 Nothing completed yet!'}
</p>
)}
</div>
);
};
export default TodoApp;
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Create type-safe React hooks with confidence 💪
- ✅ Avoid common TypeScript + React mistakes that trip up beginners 🛡️
- ✅ Apply best practices in real React projects 🎯
- ✅ Debug hook-related issues like a pro 🐛
- ✅ Build awesome React apps with TypeScript! 🚀
Remember: TypeScript + React Hooks is a powerful combination that makes your code safer, more maintainable, and more enjoyable to write. The initial setup is worth the long-term benefits! 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered React Hooks with TypeScript!
Here’s what to do next:
- 💻 Practice with the exercises above
- 🏗️ Build a small React project using typed hooks
- 📚 Move on to our next tutorial: React Context API with TypeScript
- 🌟 Share your learning journey with the React community!
Remember: Every React TypeScript expert was once a beginner. Keep coding, keep learning, and most importantly, have fun building amazing user interfaces! 🚀
Happy coding! 🎉🚀✨