What is State?
State is data that changes over time within a component. Think of it like a chameleon that changes its color based on the environment - your component changes its appearance based on its state.
Props vs State
Understanding the difference between props and state is crucial:
| Props | State |
|---|---|
| Passed from parent | Managed within component |
| Read-only | Can be updated |
| Like function parameters | Like local variables |
| Causes re-render when changed by parent | Causes re-render when updated |
Introducing useState Hook
The useState Hook is like a special container that holds a value and provides a way to update it:
import React, { useState } from 'react';
function Counter() {
// useState returns an array: [currentValue, setterFunction]
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Anatomy of useState
Basic useState Examples
1. Simple Counter
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>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
2. Text Input
function TextInput() {
const [text, setText] = useState('');
return (
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type something..."
/>
<p>You typed: {text}</p>
<p>Character count: {text.length}</p>
</div>
);
}
3. Toggle Switch
function ToggleSwitch() {
const [isOn, setIsOn] = useState(false);
return (
<div>
<button onClick={() => setIsOn(!isOn)}>
{isOn ? 'ON' : 'OFF'}
</button>
<p>The switch is {isOn ? 'on' : 'off'}</p>
</div>
);
}
Multiple State Variables
You can use useState multiple times in a single component:
function UserProfile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(parseInt(e.target.value))}
placeholder="Age"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<div>
<h3>Profile Summary:</h3>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Email: {email}</p>
</div>
</form>
);
}
Complex State with Objects
You can store objects in state, but you must be careful to update them immutably:
function ShoppingCart() {
const [cart, setCart] = useState({
items: [],
total: 0,
discount: 0
});
const addItem = (item) => {
setCart(prevCart => ({
...prevCart,
items: [...prevCart.items, item],
total: prevCart.total + item.price
}));
};
const applyDiscount = (amount) => {
setCart(prevCart => ({
...prevCart,
discount: amount
}));
};
return (
<div>
<h2>Shopping Cart</h2>
<p>Items: {cart.items.length}</p>
<p>Total: ${cart.total}</p>
<p>Discount: ${cart.discount}</p>
<p>Final Price: ${cart.total - cart.discount}</p>
<button onClick={() => addItem({ id: 1, name: 'Book', price: 20 })}>
Add Book ($20)
</button>
<button onClick={() => applyDiscount(5)}>
Apply $5 Discount
</button>
</div>
);
}
State with Arrays
Managing arrays in state requires careful attention to immutability:
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, {
id: Date.now(),
text: inputValue,
completed: false
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
Functional Updates
When the new state depends on the previous state, use the functional form:
function Counter() {
const [count, setCount] = useState(0);
// ❌ Problematic with rapid clicks
const incrementWrong = () => {
setCount(count + 1);
setCount(count + 1); // This won't work as expected
};
// ✅ Correct way using functional update
const incrementRight = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // This works correctly
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementRight}>Increment by 2</button>
</div>
);
}
Lazy Initial State
For expensive computations, you can pass a function to useState:
function ExpensiveComponent() {
// ❌ This runs on every render
const [data, setData] = useState(expensiveCalculation());
// ✅ This runs only on initial render
const [data, setData] = useState(() => expensiveCalculation());
return <div>{/* component content */}</div>;
}
// Example with localStorage
function PersistentCounter() {
const [count, setCount] = useState(() => {
const saved = localStorage.getItem('count');
return saved ? parseInt(saved) : 0;
});
useEffect(() => {
localStorage.setItem('count', count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
State Update Batching
React batches state updates for performance:
function BatchingExample() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// React batches these updates
setCount(c => c + 1); // Does not re-render yet
setFlag(f => !f); // Does not re-render yet
// React will only re-render once at the end
}
console.log('Rendered with count:', count, 'and flag:', flag);
return (
<div>
<button onClick={handleClick}>
Update Both States
</button>
<p>Count: {count}, Flag: {flag.toString()}</p>
</div>
);
}
Common useState Patterns
1. Form Handling
function Form() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Username"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
<button type="submit">Submit</button>
</form>
);
}
2. Modal Management
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>
Open Modal
</button>
{isModalOpen && (
<Modal onClose={() => setIsModalOpen(false)}>
<h2>Modal Content</h2>
<p>This is a modal dialog</p>
</Modal>
)}
</div>
);
}
3. Loading States
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/data');
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <button onClick={fetchData}>Load Data</button>;
return <div>{/* Display data */}</div>;
}
Rules of Hooks
// ❌ WRONG - Don't call Hooks inside conditions
function BadComponent({ shouldTrack }) {
if (shouldTrack) {
const [count, setCount] = useState(0); // Error!
}
// ...
}
// ✅ CORRECT - Call Hooks at the top level
function GoodComponent({ shouldTrack }) {
const [count, setCount] = useState(0);
if (shouldTrack) {
// Use the state conditionally
}
// ...
}
Common Mistakes to Avoid
1. Directly Mutating State
// ❌ WRONG
function Wrong() {
const [user, setUser] = useState({ name: 'John', age: 30 });
const updateAge = () => {
user.age = 31; // Direct mutation!
setUser(user); // React won't detect the change
};
}
// ✅ CORRECT
function Correct() {
const [user, setUser] = useState({ name: 'John', age: 30 });
const updateAge = () => {
setUser(prevUser => ({
...prevUser,
age: 31
}));
};
}
2. Stale Closure Issues
// ❌ Problem with setInterval
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always uses initial count value
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps array
return <div>Count: {count}</div>;
}
// ✅ Solution using functional update
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // Uses current value
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Count: {count}</div>;
}