Introduction to React State
State is one of the core concepts in React that allows components to create and manage their own data. Unlike props, which are passed down from parent components, state is internal to a component and can be updated over time.
Before React 16.8, state could only be used in class components. With the introduction of Hooks, functional components can now use state and other React features without writing a class.
The useState Hook
useState is a Hook that allows you to add React state to functional components.
It's the most basic Hook in React and serves as the foundation for state management in function components.
Anatomy of useState
// Basic syntax
const [state, setState] = useState(initialState);
The useState Hook returns a pair:
- The current state value
- A function that lets you update the state
Real-world analogy: Think of useState like a hotel room safe. You put something in (initial state), you get a key (state variable) to see what's inside, and a passcode (setter function) that allows you to change the contents. Only you have access to this safe, and you can update its contents whenever you need to.
Basic Example: Counter
Let's start with a simple counter example to understand how useState works:
import React, { useState } from 'react';
function Counter() {
// Declare a state variable named "count" with initial value of 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
In this example:
- We import the useState Hook from React
- We declare a state variable called "count" initialized to 0
- We get a function called "setCount" that allows us to update the count
- When the button is clicked, we call setCount with the new value
Behind the scenes: React remembers the current value of count between renders and provides the most up-to-date value to our function. When we call setCount, React re-renders the Counter component, passing the new count value to it.
Working with Different Data Types
useState can work with any data type - not just numbers, but also strings, booleans, arrays, objects, or any combination of these.
String Example: Form Input
function NameForm() {
const [name, setName] = useState('');
return (
<form>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Hello, {name || 'stranger'}!</p>
</form>
);
}
Object Example: User Profile
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
bio: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prevUser => ({
...prevUser, // Keep all other user properties
[name]: value // Update only the changed field
}));
};
return (
<form>
<input
name="name"
value={user.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="bio"
value={user.bio}
onChange={handleChange}
placeholder="Bio"
/>
</form>
);
}
Important: When using objects or arrays in state, always treat state as immutable. Never directly modify the state object - instead, create a new copy with the changes you want.
State Updates and Re-Rendering
When you call the state setter function, React:
- Updates the internal state value
- Triggers a re-render of the component
- Provides the new state value to your component
Functional Updates
If the new state depends on the previous state, it's recommended to use the functional update form:
// Instead of this:
setCount(count + 1);
// Do this:
setCount(prevCount => prevCount + 1);
Why? React state updates may be batched for performance. Using the functional form ensures you're always working with the most current state value, even if multiple updates are queued.
Real-world example: Like counter in social media
function LikeButton() {
const [likes, setLikes] = useState(0);
const [isLiked, setIsLiked] = useState(false);
const handleLike = () => {
if (isLiked) {
setLikes(prevLikes => prevLikes - 1);
setIsLiked(false);
} else {
setLikes(prevLikes => prevLikes + 1);
setIsLiked(true);
}
};
return (
<div>
<button
onClick={handleLike}
style={{ color: isLiked ? 'red' : 'gray' }}
>
{isLiked ? '❤️' : '🤍'} Like
</button>
<span>{likes} likes</span>
</div>
);
}
Multiple State Variables
You can use multiple useState Hooks in a single component to manage different pieces of state separately.
function UserForm() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
try {
// API call would go here
await new Promise(resolve => setTimeout(resolve, 1000));
alert(`Submitted: ${username}`);
setUsername('');
setPassword('');
} catch (err) {
setError('Submission failed');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{error && <p style={{ color: 'red' }}>{error}</p>}
<div>
<label>
Username:
<input
value={username}
onChange={e => setUsername(e.target.value)}
disabled={isSubmitting}
/>
</label>
</div>
<div>
<label>
Password:
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
disabled={isSubmitting}
/>
</label>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Decision point: When deciding whether to use multiple state variables or an object, consider:
- Use separate state variables for unrelated data
- Group related data in an object
- Consider how the data changes together - if fields always update together, group them
Lazy Initial State
If your initial state is the result of an expensive computation, you can provide a function to useState which will only be executed on the initial render.
// Instead of this:
const [state, setState] = useState(expensiveComputation());
// Do this:
const [state, setState] = useState(() => expensiveComputation());
Example: Loading saved preferences
function UserPreferences() {
// This function only runs once on initial render
const [preferences, setPreferences] = useState(() => {
// Potentially expensive operation
const savedPrefs = localStorage.getItem('userPreferences');
return savedPrefs ? JSON.parse(savedPrefs) : {
theme: 'light',
fontSize: 'medium',
notifications: true
};
});
const updatePreference = (key, value) => {
const newPreferences = { ...preferences, [key]: value };
setPreferences(newPreferences);
localStorage.setItem('userPreferences', JSON.stringify(newPreferences));
};
return (
<div>
<h3>Your Preferences</h3>
<div>
<label>
Theme:
<select
value={preferences.theme}
onChange={e => updatePreference('theme', e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</div>
<div>
<label>
Font Size:
<select
value={preferences.fontSize}
onChange={e => updatePreference('fontSize', e.target.value)}
>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</label>
</div>
<div>
<label>
Notifications:
<input
type="checkbox"
checked={preferences.notifications}
onChange={e => updatePreference('notifications', e.target.checked)}
/>
</label>
</div>
</div>
);
}
Common Pitfalls and Best Practices
State Updates are Asynchronous
React may batch multiple state updates for performance. Don't rely on previous state values for calculating the next state directly.
❌ Incorrect
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
setCount(count + 1); // Will not work as expected
};
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
// Clicking will only increment by 1, not 2
✅ Correct
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
// Clicking will increment by 2
State is Replaced, Not Merged
Unlike this.setState in class components, the state setter from useState doesn't automatically merge objects.
❌ Incorrect
function UserProfile() {
const [user, setUser] = useState({
name: 'John',
email: 'john@example.com'
});
const updateEmail = (newEmail) => {
// This will remove the name property!
setUser({ email: newEmail });
};
}
✅ Correct
function UserProfile() {
const [user, setUser] = useState({
name: 'John',
email: 'john@example.com'
});
const updateEmail = (newEmail) => {
// Spread the previous state and only update email
setUser(prevUser => ({
...prevUser,
email: newEmail
}));
};
}
Real-World Applications
Shopping Cart Component
A practical example of using useState for managing a shopping cart:
function ShoppingCart() {
const [cart, setCart] = useState([]);
const [total, setTotal] = useState(0);
const addItem = (product) => {
// Check if product already exists in cart
const existingItemIndex = cart.findIndex(
item => item.id === product.id
);
if (existingItemIndex >= 0) {
// Product exists, increment quantity
const updatedCart = [...cart];
updatedCart[existingItemIndex] = {
...updatedCart[existingItemIndex],
quantity: updatedCart[existingItemIndex].quantity + 1
};
setCart(updatedCart);
} else {
// Add new product with quantity 1
setCart([...cart, { ...product, quantity: 1 }]);
}
// Update total
setTotal(prevTotal => prevTotal + product.price);
};
const removeItem = (productId) => {
const itemIndex = cart.findIndex(item => item.id === productId);
if (itemIndex >= 0) {
const item = cart[itemIndex];
// Update total
setTotal(prevTotal => prevTotal - (item.price * item.quantity));
// Remove item from cart
setCart(cart.filter(item => item.id !== productId));
}
};
return (
<div className="shopping-cart">
<h2>Your Cart ({cart.length} items)</h2>
{cart.length === 0 ? (
<p>Your cart is empty</p>
) : (
<ul>
{cart.map(item => (
<li key={item.id}>
{item.name} - ${item.price} x {item.quantity}
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
)}
<div className="cart-total">
<strong>Total: ${total.toFixed(2)}</strong>
</div>
{/* Product list and add buttons would go here */}
</div>
);
}
Toggle Theme Feature
A common UI feature implementing dark/light mode:
function ThemeToggle() {
// Check if user has a saved preference
const [theme, setTheme] = useState(() => {
const savedTheme = localStorage.getItem('theme');
return savedTheme || 'light';
});
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
// Also apply theme to document
document.body.className = newTheme;
};
// Apply theme on initial render
React.useEffect(() => {
document.body.className = theme;
}, [theme]);
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
</button>
);
}
When to Use useState vs Other State Management
useState is perfect for:
- Local component state that doesn't need to be shared
- Simple state logic that doesn't involve complex transitions
- Independent pieces of state that don't rely on each other
Consider other options when:
- State needs to be shared across many components (Context API, Redux)
- State logic is complex with many transitions (useReducer)
- You have deeply nested state objects (useReducer)
Practice Exercises
Exercise 1: Counter App
Create a counter app with increment, decrement, and reset buttons. Add a feature that prevents the counter from going below 0.
Exercise 2: Form with Validation
Build a form with at least three fields (name, email, password) and implement:
- Real-time validation for each field
- Disable submit button until all fields are valid
- Show success message on submission
Exercise 3: Todo List
Create a todo list application with the following features:
- Add new todos
- Mark todos as completed
- Delete todos
- Filter todos by status (all, active, completed)
Summary
In this lecture, we've learned:
- How to use the useState Hook to add state to functional components
- Working with different data types in state
- How to properly update state, especially for objects and arrays
- Using the functional update form for state that depends on previous state
- Common patterns and pitfalls when working with useState
- Real-world examples of state management with functional components
The useState Hook is a fundamental building block for React applications, allowing you to create dynamic, interactive components without class syntax. Understanding how to use it effectively will make you more productive as a React developer.