Table of Contents
What is State in React?
State is a built-in React object that allows components to create and manage their own data. Unlike props, which are passed down from parent components, state is managed locally within a component and can change over time, triggering re-renders when updated.
Understanding State vs Props
State | Props |
---|---|
Managed within the component | Passed from parent component |
Can be changed (mutable) | Read-only (immutable) |
Triggers re-render when changed | Received from parent |
Private to the component | Can be shared between components |
Using useState Hook
The useState hook is the primary way to add state to functional components:
import React, { useState } from 'react';
function Counter() {
// Declare state variable with initial value
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
return (
<div className="counter">
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
State with Different Data Types
State can hold various types of data:
function UserProfile() {
// String state
const [name, setName] = useState("John Doe");
// Number state
const [age, setAge] = useState(25);
// Boolean state
const [isVisible, setIsVisible] = useState(true);
// Array state
const [hobbies, setHobbies] = useState(["Reading", "Gaming"]);
// Object state
const [user, setUser] = useState({
email: "john@example.com",
location: "New York",
preferences: {
theme: "dark",
notifications: true
}
});
const addHobby = () => {
const newHobby = prompt("Enter a new hobby:");
if (newHobby) {
setHobbies([...hobbies, newHobby]);
}
};
const updateEmail = () => {
const newEmail = prompt("Enter new email:");
if (newEmail) {
setUser({
...user,
email: newEmail
});
}
};
const toggleTheme = () => {
setUser({
...user,
preferences: {
...user.preferences,
theme: user.preferences.theme === "dark" ? "light" : "dark"
}
});
};
return (
<div className="user-profile">
{isVisible && (
<div>
<h2>{name}, {age} years old</h2>
<p>Email: {user.email}</p>
<p>Location: {user.location}</p>
<p>Theme: {user.preferences.theme}</p>
<div>
<h3>Hobbies:</h3>
<ul>
{hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
<button onClick={addHobby}>Add Hobby</button>
</div>
<div>
<button onClick={updateEmail}>Update Email</button>
<button onClick={toggleTheme}>Toggle Theme</button>
<button onClick={() => setIsVisible(false)}>
Hide Profile
</button>
</div>
</div>
)}
{!isVisible && (
<button onClick={() => setIsVisible(true)}>
Show Profile
</button>
)}
</div>
);
}
Functional State Updates
When the new state depends on the previous state, use functional updates:
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState("");
const addTodo = () => {
if (inputValue.trim()) {
setTodos(prevTodos => [
...prevTodos,
{
id: Date.now(),
text: inputValue,
completed: false
}
]);
setInputValue("");
}
};
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
};
const deleteTodo = (id) => {
setTodos(prevTodos =>
prevTodos.filter(todo => todo.id !== id)
);
};
return (
<div className="todo-list">
<h2>Todo List</h2>
<div className="add-todo">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Enter a new todo"
onKeyPress={(e) => e.key === "Enter" && addTodo()}
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? "completed" : ""}>
<span onClick={() => toggleTodo(todo.id)}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
<p>
Total: {todos.length} |
Completed: {todos.filter(t => t.completed).length} |
Remaining: {todos.filter(t => !t.completed).length}
</p>
</div>
);
}
Multiple State Variables vs Single State Object
You can choose between multiple useState calls or a single state object:
// Multiple state variables (recommended for simple, unrelated data)
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
// ... component logic
}
// Single state object (good for related data)
function LoginForm() {
const [formState, setFormState] = useState({
email: "",
password: "",
isLoading: false,
error: ""
});
const updateField = (field, value) => {
setFormState(prev => ({
...prev,
[field]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setFormState(prev => ({
...prev,
isLoading: true,
error: ""
}));
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
console.log("Login successful!");
} catch (err) {
setFormState(prev => ({
...prev,
error: "Login failed. Please try again."
}));
} finally {
setFormState(prev => ({
...prev,
isLoading: false
}));
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={formState.email}
onChange={(e) => updateField("email", e.target.value)}
placeholder="Email"
required
/>
</div>
<div>
<input
type="password"
value={formState.password}
onChange={(e) => updateField("password", e.target.value)}
placeholder="Password"
required
/>
</div>
{formState.error && (
<div className="error">{formState.error}</div>
)}
<button type="submit" disabled={formState.isLoading}>
{formState.isLoading ? "Logging in..." : "Login"}
</button>
</form>
);
}
State and Event Handling
State is commonly updated in response to user events:
function InteractiveCard() {
const [isExpanded, setIsExpanded] = useState(false);
const [likes, setLikes] = useState(0);
const [isLiked, setIsLiked] = useState(false);
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
const handleLike = () => {
if (isLiked) {
setLikes(likes - 1);
setIsLiked(false);
} else {
setLikes(likes + 1);
setIsLiked(true);
}
};
return (
<div className="interactive-card">
<div className="card-header">
<h3>Interactive Card</h3>
<button onClick={toggleExpanded}>
{isExpanded ? "Collapse" : "Expand"}
</button>
</div>
<div className="card-content">
<p>This is always visible content.</p>
{isExpanded && (
<div className="expanded-content">
<p>This content is only visible when expanded!</p>
<p>You can put any additional information here.</p>
</div>
)}
</div>
<div className="card-footer">
<button
onClick={handleLike}
className={isLiked ? "liked" : ""}
>
❤️ {likes} {likes === 1 ? "Like" : "Likes"}
</button>
</div>
</div>
);
}
Practical Exercise: Shopping Cart
function ShoppingCart() {
const [items, setItems] = useState([]);
const [newItem, setNewItem] = useState({ name: "", price: "" });
const addItem = () => {
if (newItem.name && newItem.price) {
setItems(prevItems => [
...prevItems,
{
id: Date.now(),
name: newItem.name,
price: parseFloat(newItem.price),
quantity: 1
}
]);
setNewItem({ name: "", price: "" });
}
};
const updateQuantity = (id, newQuantity) => {
if (newQuantity <= 0) {
removeItem(id);
return;
}
setItems(prevItems =>
prevItems.map(item =>
item.id === id
? { ...item, quantity: newQuantity }
: item
)
);
};
const removeItem = (id) => {
setItems(prevItems =>
prevItems.filter(item => item.id !== id)
);
};
const getTotalPrice = () => {
return items.reduce((total, item) =>
total + (item.price * item.quantity), 0
).toFixed(2);
};
const getTotalItems = () => {
return items.reduce((total, item) => total + item.quantity, 0);
};
return (
<div className="shopping-cart">
<h2>Shopping Cart ({getTotalItems()} items)</h2>
<div className="add-item">
<input
type="text"
placeholder="Item name"
value={newItem.name}
onChange={(e) => setNewItem({...newItem, name: e.target.value})}
/>
<input
type="number"
placeholder="Price"
value={newItem.price}
onChange={(e) => setNewItem({...newItem, price: e.target.value})}
/>
<button onClick={addItem}>Add Item</button>
</div>
<div className="cart-items">
{items.length === 0 ? (
<p>Your cart is empty</p>
) : (
items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name}</span>
<span>${item.price.toFixed(2)}</span>
<div>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
</div>
<button onClick={() => removeItem(item.id)}>
Remove
</button>
</div>
))
)}
</div>
{items.length > 0 && (
<div className="cart-total">
<h3>Total: ${getTotalPrice()}</h3>
<button onClick={() => setItems([])}>
Clear Cart
</button>
</div>
)}
</div>
);
}
Best Practices for State Management
- Keep state minimal: Only store what you need in state
- Use functional updates: When new state depends on previous state
- Don't mutate state directly: Always create new objects/arrays
- Group related state: Use objects for related data
- Initialize state properly: Provide appropriate initial values
- Use multiple useState calls: For unrelated pieces of state
Common Mistakes to Avoid
- Mutating state directly: Always use the setter function
- Not using functional updates: Can lead to stale state issues
- Too much state: Not everything needs to be in state
- Forgetting dependencies: In useEffect (covered in later lessons)
Summary
State is fundamental to creating interactive React applications. You've learned how to use the useState hook, manage different types of state data, handle user interactions, and follow best practices for state management. In the next lesson, we'll explore event handling in more detail and learn how to create more interactive user interfaces.