Learn React Hooks including useState, useEffect, useContext, and custom hooks. Master modern React development patterns.
Learn React Hooks including useState, useEffect, useContext, and custom hooks. Master modern React development patterns.
Understand the fundamentals of React Hooks and why they were introduced
Content by: Kriyansh Khunt
MERN Stack Developer
React Hooks are functions that allow you to use state and other React features in function components. They were introduced in React 16.8 to allow you to use state and other React features without writing a class.
import React, { useState } from 'react';
function Example() {
// Declare a new state variable called "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
// Before Hooks (Class Component)
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
// Class Component with multiple state
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
age: 0,
isEditing: false
};
}
handleNameChange = (e) => {
this.setState({ name: e.target.value });
};
handleEmailChange = (e) => {
this.setState({ email: e.target.value });
};
toggleEditing = () => {
this.setState(prevState => ({
isEditing: !prevState.isEditing
}));
};
render() {
return (
<div>
{this.state.isEditing ? (
<div>
<input
value={this.state.name}
onChange={this.handleNameChange}
/>
<input
value={this.state.email}
onChange={this.handleEmailChange}
/>
<button onClick={this.toggleEditing}>Save</button>
</div>
) : (
<div>
<h2>{this.state.name}</h2>
<p>{this.state.email}</p>
<button onClick={this.toggleEditing}>Edit</button>
</div>
)}
</div>
);
}
}
// Function Component with Hooks (equivalent)
function UserProfile() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const [isEditing, setIsEditing] = useState(false);
const toggleEditing = () => {
setIsEditing(!isEditing);
};
return (
<div>
{isEditing ? (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button onClick={toggleEditing}>Save</button>
</div>
) : (
<div>
<h2>{name}</h2>
<p>{email}</p>
<button onClick={toggleEditing}>Edit</button>
</div>
)}
</div>
);
}
// Advantages of Hooks:
// - Less code and boilerplate
// - Easier to understand and test
// - Better performance optimization
// - Reusable logic between components
// Exercise: Convert a Class Component to Function Component with Hooks
// Convert this class component to use hooks
// Original Class Component
class TodoApp extends React.Component {
constructor(props) {
super(props);
this.state = {
todos: [],
inputValue: '',
filter: 'all'
};
}
componentDidMount() {
// Load todos from localStorage
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
this.setState({ todos: JSON.parse(savedTodos) });
}
}
componentDidUpdate(prevProps, prevState) {
// Save todos to localStorage when they change
if (prevState.todos !== this.state.todos) {
localStorage.setItem('todos', JSON.stringify(this.state.todos));
}
}
addTodo = () => {
if (this.state.inputValue.trim()) {
this.setState(prevState => ({
todos: [...prevState.todos, {
id: Date.now(),
text: prevState.inputValue,
completed: false
}],
inputValue: ''
}));
}
};
toggleTodo = (id) => {
this.setState(prevState => ({
todos: prevState.todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
}));
};
deleteTodo = (id) => {
this.setState(prevState => ({
todos: prevState.todos.filter(todo => todo.id !== id)
}));
};
setFilter = (filter) => {
this.setState({ filter });
};
render() {
const filteredTodos = this.state.todos.filter(todo => {
if (this.state.filter === 'active') return !todo.completed;
if (this.state.filter === 'completed') return todo.completed;
return true;
});
return (
<div>
<input
value={this.state.inputValue}
onChange={(e) => this.setState({ inputValue: e.target.value })}
placeholder="Add todo"
/>
<button onClick={this.addTodo}>Add</button>
<div>
<button onClick={() => this.setFilter('all')}>All</button>
<button onClick={() => this.setFilter('active')}>Active</button>
<button onClick={() => this.setFilter('completed')}>Completed</button>
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => this.toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => this.deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
}
// Your Task: Convert the above class component to a function component using hooks
// Use useState for state management
// Use useEffect for side effects (localStorage)
// Maintain the same functionality
// Hint: You'll need:
// - useState for todos, inputValue, and filter
// - useEffect for localStorage operations
// - Event handlers as regular functions
// Challenge: Add a custom hook for localStorage management
// Challenge: Add a custom hook for todo filtering
// Challenge: Add a custom hook for todo operations
Test your understanding of this topic:
Master the useState hook for managing component state
Content by: Navin Kumar
MERN Stack Developer
The useState hook is a React Hook that lets you add state to function components. It returns an array with the current state value and a function to update it.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<button onClick={() => setCount(count - 1)}>
Decrement
</button>
</div>
);
}
// Multiple state variables
function UserProfile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Age"
type="number"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
</div>
);
}
// Managing object state
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateUser = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
return (
<div>
<input
value={user.name}
onChange={(e) => updateUser('name', e.target.value)}
placeholder="Name"
/>
<input
value={user.email}
onChange={(e) => updateUser('email', e.target.value)}
placeholder="Email"
/>
<input
value={user.age}
onChange={(e) => updateUser('age', Number(e.target.value))}
placeholder="Age"
type="number"
/>
</div>
);
}
// Managing arrays with useState
function TodoList() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
// Add new todo
const addTodo = () => {
if (newTodo.trim()) {
setTodos(prevTodos => [...prevTodos, {
id: Date.now(),
text: newTodo,
completed: false
}]);
setNewTodo('');
}
};
// Toggle todo completion
const toggleTodo = (id) => {
setTodos(prevTodos => prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
// Delete todo
const deleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
// Update todo text
const updateTodo = (id, newText) => {
setTodos(prevTodos => prevTodos.map(todo =>
todo.id === id
? { ...todo, text: newText }
: todo
));
};
return (
<div>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add new todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<input
value={todo.text}
onChange={(e) => updateTodo(todo.id, e.target.value)}
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
/>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
// Complex state with nested objects and arrays
function ShoppingCart() {
const [cart, setCart] = useState({
items: [],
total: 0,
discount: 0,
shipping: 0
});
const addItem = (product) => {
setCart(prevCart => {
const existingItem = prevCart.items.find(item => item.id === product.id);
if (existingItem) {
// Update quantity of existing item
const updatedItems = prevCart.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
return {
...prevCart,
items: updatedItems,
total: calculateTotal(updatedItems, prevCart.discount, prevCart.shipping)
};
} else {
// Add new item
const newItems = [...prevCart.items, { ...product, quantity: 1 }];
return {
...prevCart,
items: newItems,
total: calculateTotal(newItems, prevCart.discount, prevCart.shipping)
};
}
});
};
const updateQuantity = (productId, quantity) => {
setCart(prevCart => {
const updatedItems = prevCart.items.map(item =>
item.id === productId
? { ...item, quantity: Math.max(0, quantity) }
: item
).filter(item => item.quantity > 0); // Remove items with 0 quantity
return {
...prevCart,
items: updatedItems,
total: calculateTotal(updatedItems, prevCart.discount, prevCart.shipping)
};
});
};
const applyDiscount = (discountPercent) => {
setCart(prevCart => ({
...prevCart,
discount: discountPercent,
total: calculateTotal(prevCart.items, discountPercent, prevCart.shipping)
}));
};
const calculateTotal = (items, discount, shipping) => {
const subtotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const discountAmount = (subtotal * discount) / 100;
return subtotal - discountAmount + shipping;
};
return (
<div>
<h2>Shopping Cart</h2>
{cart.items.map(item => (
<div key={item.id}>
<span>{item.name}</span>
<span>{item.price}</span>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
min="0"
/>
</div>
))}
<div>
<strong>Total: {cart.total.toFixed(2)}</strong>
</div>
<button onClick={() => applyDiscount(10)}>Apply 10% Discount</button>
</div>
);
}
// Exercise: Build a Task Management System
// Create a comprehensive task management system with multiple state patterns
// src/components/TaskManager.js
import React, { useState } from 'react';
function TaskManager() {
const [tasks, setTasks] = useState([]);
const [categories, setCategories] = useState([
{ id: 1, name: 'Work', color: '#ff6b6b' },
{ id: 2, name: 'Personal', color: '#4ecdc4' },
{ id: 3, name: 'Shopping', color: '#45b7d1' }
]);
const [filters, setFilters] = useState({
category: 'all',
status: 'all',
priority: 'all'
});
const [newTask, setNewTask] = useState({
title: '',
description: '',
categoryId: 1,
priority: 'medium',
dueDate: ''
});
// Add new task
const addTask = () => {
if (newTask.title.trim()) {
const task = {
id: Date.now(),
...newTask,
status: 'pending',
createdAt: new Date().toISOString(),
completedAt: null
};
setTasks(prevTasks => [...prevTasks, task]);
setNewTask({
title: '',
description: '',
categoryId: 1,
priority: 'medium',
dueDate: ''
});
}
};
// Update task status
const updateTaskStatus = (taskId, status) => {
setTasks(prevTasks => prevTasks.map(task =>
task.id === taskId
? {
...task,
status,
completedAt: status === 'completed' ? new Date().toISOString() : null
}
: task
));
};
// Delete task
const deleteTask = (taskId) => {
setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId));
};
// Filter tasks
const getFilteredTasks = () => {
return tasks.filter(task => {
const categoryMatch = filters.category === 'all' || task.categoryId === parseInt(filters.category);
const statusMatch = filters.status === 'all' || task.status === filters.status;
const priorityMatch = filters.priority === 'all' || task.priority === filters.priority;
return categoryMatch && statusMatch && priorityMatch;
});
};
// Get category by ID
const getCategoryById = (categoryId) => {
return categories.find(cat => cat.id === categoryId);
};
// Get priority color
const getPriorityColor = (priority) => {
const colors = {
low: '#28a745',
medium: '#ffc107',
high: '#dc3545'
};
return colors[priority] || '#6c757d';
};
const filteredTasks = getFilteredTasks();
return (
<div className="task-manager">
{/* Add Task Form */}
<div className="add-task-form">
<h3>Add New Task</h3>
<input
value={newTask.title}
onChange={(e) => setNewTask(prev => ({ ...prev, title: e.target.value }))}
placeholder="Task title"
/>
<textarea
value={newTask.description}
onChange={(e) => setNewTask(prev => ({ ...prev, description: e.target.value }))}
placeholder="Task description"
/>
<select
value={newTask.categoryId}
onChange={(e) => setNewTask(prev => ({ ...prev, categoryId: parseInt(e.target.value) }))}
>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>{cat.name}</option>
))}
</select>
<select
value={newTask.priority}
onChange={(e) => setNewTask(prev => ({ ...prev, priority: e.target.value }))}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<input
type="date"
value={newTask.dueDate}
onChange={(e) => setNewTask(prev => ({ ...prev, dueDate: e.target.value }))}
/>
<button onClick={addTask}>Add Task</button>
</div>
{/* Filters */}
<div className="filters">
<select
value={filters.category}
onChange={(e) => setFilters(prev => ({ ...prev, category: e.target.value }))}
>
<option value="all">All Categories</option>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>{cat.name}</option>
))}
</select>
<select
value={filters.status}
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value }))}
>
<option value="all">All Status</option>
<option value="pending">Pending</option>
<option value="in-progress">In Progress</option>
<option value="completed">Completed</option>
</select>
<select
value={filters.priority}
onChange={(e) => setFilters(prev => ({ ...prev, priority: e.target.value }))}
>
<option value="all">All Priorities</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
{/* Task List */}
<div className="task-list">
<h3>Tasks ({filteredTasks.length})</h3>
{filteredTasks.map(task => {
const category = getCategoryById(task.categoryId);
return (
<div key={task.id} className="task-item">
<div className="task-header">
<h4>{task.title}</h4>
<span
className="priority-badge"
style={{ backgroundColor: getPriorityColor(task.priority) }}
>
{task.priority}
</span>
<span
className="category-badge"
style={{ backgroundColor: category.color }}
>
{category.name}
</span>
</div>
<p>{task.description}</p>
<div className="task-meta">
<span>Due: {task.dueDate}</span>
<span>Status: {task.status}</span>
</div>
<div className="task-actions">
<select
value={task.status}
onChange={(e) => updateTaskStatus(task.id, e.target.value)}
>
<option value="pending">Pending</option>
<option value="in-progress">In Progress</option>
<option value="completed">Completed</option>
</select>
<button onClick={() => deleteTask(task.id)}>Delete</button>
</div>
</div>
);
})}
</div>
</div>
);
}
// Challenge: Add task editing functionality
// Challenge: Add task search functionality
// Challenge: Add task statistics and analytics
// Challenge: Add task export/import functionality
Test your understanding of this topic:
Learn to handle side effects in React components with useEffect
Content by: Sejal Mishra
MERN Stack Developer
The useEffect hook lets you perform side effects in function components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
// useEffect with dependency array
function Example() {
const [count, setCount] = useState(0);
// Only run when count changes
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
// Cleanup function (similar to componentWillUnmount)
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(timer);
};
}, []);
return <div>Count: {count}</div>;
}
// Fetching data with useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Error fetching user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Test your understanding of this topic:
Create reusable custom hooks to share stateful logic between components
Content by: Harshil Dudhat
MERN Stack Developer
Custom hooks are JavaScript functions that start with 'use' and may call other hooks. They allow you to extract component logic into reusable functions.
// Custom hook for form input
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = (e) => {
setValue(e.target.value);
};
const reset = () => {
setValue(initialValue);
};
return [value, handleChange, reset];
}
// Using the custom hook
function LoginForm() {
const [username, handleUsernameChange, resetUsername] = useInput('');
const [password, handlePasswordChange, resetPassword] = useInput('');
const handleSubmit = (e) => {
e.preventDefault();
resetUsername();
resetPassword();
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={username}
onChange={handleUsernameChange}
placeholder="Username"
/>
<input
type="password"
value={password}
onChange={handlePasswordChange}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
}
// Custom hook for API calls
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Using the custom hook
function UserList() {
const { data: users, loading, error } = useApi('/api/users');
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
if (!users) return <div>No users found</div>;
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 3