State with useState
State - Making Components Interactive
State is data that changes over time. While props are passed from parent to child, state is managed within the component itself. State is what makes React components interactive.
What is State?
State is a component’s memory - data it needs to remember between renders:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Every time you click, count updates and the component re-renders with the new value.
The useState Hook
useState is React’s way to add state to function components:
const [value, setValue] = useState(initialValue);
Returns an array with two elements:
- Current value - the current state
- Setter function - function to update the state
Naming convention: [thing, setThing]
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [items, setItems] = useState([]);
Basic useState Examples
Number:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
String:
function NameInput() {
const [name, setName] = useState('');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
);
}
Boolean:
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? 'ON' : 'OFF'}
</button>
);
}
Array:
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text }]);
};
return (
<div>
<button onClick={() => addTodo('New todo')}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
Object:
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
const updateName = (name) => {
setUser({ ...user, name });
};
return (
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
/>
);
}
Updating State
Replace the value:
const [count, setCount] = useState(0);
setCount(5); // Set to 5
setCount(count + 1); // Increment
Functional update (when new state depends on old state):
// ⚠️ Can be unreliable
setCount(count + 1);
// ✅ Always reliable
setCount(prevCount => prevCount + 1);
Why functional updates matter:
function Counter() {
const [count, setCount] = useState(0);
const incrementThrice = () => {
// Wrong - all use same count value
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1
// Result: count is 1
// Right - each uses previous update
setCount(c => c + 1); // 0 + 1 = 1
setCount(c => c + 1); // 1 + 1 = 2
setCount(c => c + 1); // 2 + 1 = 3
// Result: count is 3
};
return <button onClick={incrementThrice}>{count}</button>;
}
Updating Objects
Never mutate state directly:
// ❌ Wrong - mutating state
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.age = 26; // Don't do this!
setUser(user); // Won't trigger re-render
// ✅ Right - create new object
setUser({ ...user, age: 26 });
// ✅ Right - functional update
setUser(prev => ({ ...prev, age: 26 }));
Updating nested objects:
const [user, setUser] = useState({
name: 'Alice',
address: {
city: 'NYC',
country: 'USA'
}
});
// Update nested property
setUser({
...user,
address: {
...user.address,
city: 'LA'
}
});
Updating Arrays
Add item:
const [items, setItems] = useState([1, 2, 3]);
// At end
setItems([...items, 4]); // [1, 2, 3, 4]
// At beginning
setItems([0, ...items]); // [0, 1, 2, 3]
// At specific position
const newItems = [...items];
newItems.splice(2, 0, 2.5); // Insert at index 2
setItems(newItems);
Remove item:
// By index
setItems(items.filter((_, index) => index !== 2));
// By value
setItems(items.filter(item => item.id !== targetId));
Update item:
setItems(items.map(item =>
item.id === targetId
? { ...item, completed: true }
: item
));
Replace array:
setItems([5, 6, 7]);
Multiple State Variables
Components can have multiple useState calls:
function UserProfile() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const [isActive, setIsActive] = useState(true);
const [preferences, setPreferences] = useState({});
return (
// Use all these state variables
<div>...</div>
);
}
Group related state:
// Instead of separate states
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
// Consider grouping
const [user, setUser] = useState({
firstName: '',
lastName: '',
email: ''
});
State and Re-rendering
When state changes, React re-renders:
function Example() {
const [count, setCount] = useState(0);
console.log('Component rendered!'); // Logs on every render
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
// Click logs "Component rendered!" each time
State updates are batched:
function Example() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
setCount(count + 1); // Scheduled
setFlag(!flag); // Scheduled
// Both updates batched into one re-render
};
return <button onClick={handleClick}>Update</button>;
}
Practical Examples
Example 1: Todo List
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([
...todos,
{ id: Date.now(), text: input, completed: false }
]);
setInput('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<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={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Example 2: Form with Validation
function SignupForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error for this field
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
const validate = () => {
const newErrors = {};
if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}
if (!formData.email.includes('@')) {
newErrors.email = 'Invalid email';
}
if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Username"
/>
{errors.username && <span className="error">{errors.username}</span>}
</div>
<div>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Sign Up</button>
</form>
);
}
Example 3: Counter with Limits
function LimitedCounter() {
const [count, setCount] = useState(0);
const [min] = useState(0);
const [max] = useState(10);
const increment = () => {
setCount(c => Math.min(c + 1, max));
};
const decrement = () => {
setCount(c => Math.max(c - 1, min));
};
const reset = () => {
setCount(0);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={decrement} disabled={count <= min}>
-
</button>
<button onClick={reset}>Reset</button>
<button onClick={increment} disabled={count >= max}>
+
</button>
<p>Range: {min} - {max}</p>
</div>
);
}
Example 4: Modal Toggle
function ModalExample() {
const [isOpen, setIsOpen] = useState(false);
const [modalContent, setModalContent] = useState('');
const openModal = (content) => {
setModalContent(content);
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
return (
<div>
<button onClick={() => openModal('Welcome message!')}>
Open Modal
</button>
{isOpen && (
<div className="modal-overlay" onClick={closeModal}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>{modalContent}</h2>
<button onClick={closeModal}>Close</button>
</div>
</div>
)}
</div>
);
}
State Initialization
Simple value:
const [count, setCount] = useState(0);
Computed initial value (runs once):
const [value, setValue] = useState(() => {
const stored = localStorage.getItem('myValue');
return stored ? JSON.parse(stored) : defaultValue;
});
Only use function form for expensive computations:
// Good - expensive operation runs once
const [data, setData] = useState(() => processLargeDataset());
// Bad - expensive operation runs every render
const [data, setData] = useState(processLargeDataset());
Common Patterns
1. Derived state (calculate from existing state):
function ShoppingCart() {
const [items, setItems] = useState([]);
// Don't store total in state - calculate it
const total = items.reduce((sum, item) => sum + item.price, 0);
const itemCount = items.length;
return (
<div>
<p>Items: {itemCount}</p>
<p>Total: ${total}</p>
</div>
);
}
2. Resetting state:
function Form() {
const initialState = { name: '', email: '' };
const [formData, setFormData] = useState(initialState);
const reset = () => {
setFormData(initialState);
};
return (
<div>
{/* form inputs */}
<button onClick={reset}>Reset</button>
</div>
);
}
3. State based on previous state:
const [history, setHistory] = useState([]);
const addToHistory = (item) => {
setHistory(prev => [...prev, item].slice(-10)); // Keep last 10
};
Best Practices
1. State should be minimal:
// Bad - storing derived data
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // Redundant!
// Good - calculate derived data
const [items, setItems] = useState([]);
const count = items.length;
2. Keep related state together:
// Bad
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// Good
const [position, setPosition] = useState({ x: 0, y: 0 });
3. Don’t mirror props in state unless needed:
// Bad - props change won't update state
function Component({ initialCount }) {
const [count, setCount] = useState(initialCount);
// If prop changes, state doesn't!
}
// Good - use prop directly
function Component({ count }) {
return <div>{count}</div>;
}
// Good - only if you need to diverge from prop
function Component({ initialCount }) {
const [count, setCount] = useState(initialCount);
// State starts with prop but can change independently
}
Common Mistakes
1. Mutating state:
// Wrong
items.push(newItem);
setItems(items);
// Right
setItems([...items, newItem]);
2. Using stale state in async:
// Wrong - count might be stale
setTimeout(() => {
setCount(count + 1);
}, 1000);
// Right - use functional update
setTimeout(() => {
setCount(c => c + 1);
}, 1000);
3. Too much state:
// Bad - over-using state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // Redundant!
// Good - derive fullName
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
Next Article: useEffect Hook