useState - Managing Component State
The useState hook is the foundation of state management in functional components. It returns a state value and a function to update it, replacing the need for this.state and this.setState from class components.
Basic Usage
The simplest form of useState takes an initial value and returns an array with the current state and a setter function:
import React, { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> );}Key Points:
- Array Destructuring:
useStatereturns an array, so we use destructuring to get the value and setter - Naming Convention: The setter is typically named
set+ capitalized state name - Initial Value: Can be any type - string, number, object, array, boolean, etc.
State Updates
Direct Updates
For simple values, you can update state directly:
function TextInput() { const [text, setText] = useState('');
return ( <input value={text} onChange={(e) => setText(e.target.value)} placeholder="Type something..." /> );}Functional Updates
When the new state depends on the previous state, use a function to ensure you’re working with the latest value:
function Counter() { const [count, setCount] = useState(0);
const increment = () => { // ❌ This might not work as expected with rapid clicks // setCount(count + 1);
// ✅ This always works correctly setCount(prevCount => prevCount + 1); };
const incrementByFive = () => { // This will only increment by 1, not 5! // setCount(count + 1); // setCount(count + 1); // setCount(count + 1); // setCount(count + 1); // setCount(count + 1);
// ✅ This correctly increments by 5 setCount(prev => prev + 1); setCount(prev => prev + 1); setCount(prev => prev + 1); setCount(prev => prev + 1); setCount(prev => prev + 1); };
return ( <div> <p>Count: {count}</p> <button onClick={increment}>+1</button> <button onClick={incrementByFive}>+5</button> </div> );}Working with Objects and Arrays
Object State
Remember that useState doesn’t merge objects automatically like this.setState did in class components:
function UserProfile() { const [user, setUser] = useState({ name: '', email: '', age: 0 });
const updateName = (newName) => { // ❌ This will replace the entire object // setUser({ name: newName });
// ✅ Spread the previous state to keep other properties setUser(prevUser => ({ ...prevUser, name: newName })); };
const updateUser = (updates) => { setUser(prevUser => ({ ...prevUser, ...updates })); };
return ( <div> <input value={user.name} onChange={(e) => updateName(e.target.value)} placeholder="Name" /> <input value={user.email} onChange={(e) => updateUser({ email: e.target.value })} placeholder="Email" /> <input type="number" value={user.age} onChange={(e) => updateUser({ age: parseInt(e.target.value) })} placeholder="Age" /> <p>User: {JSON.stringify(user, null, 2)}</p> </div> );}Array State
Working with arrays requires creating new arrays rather than mutating existing ones:
function TodoList() { const [todos, setTodos] = useState([]); const [inputValue, setInputValue] = useState('');
const addTodo = () => { if (inputValue.trim()) { // ✅ Create a new array with the new todo setTodos(prevTodos => [ ...prevTodos, { id: Date.now(), text: inputValue.trim(), 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> <div> <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} placeholder="Add a todo..." /> <button onClick={addTodo}>Add</button> </div>
<ul> {todos.map(todo => ( <li key={todo.id}> <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} onClick={() => toggleTodo(todo.id)} > {todo.text} </span> <button onClick={() => deleteTodo(todo.id)}>Delete</button> </li> ))} </ul> </div> );}Lazy Initial State
If your initial state is expensive to compute, you can pass a function to useState:
function ExpensiveComponent() { // ❌ This runs on every render const [data, setData] = useState(expensiveCalculation());
// ✅ This only runs once on initial render const [data, setData] = useState(() => expensiveCalculation());
// ✅ Also works with more complex initialization const [user, setUser] = useState(() => { const savedUser = localStorage.getItem('user'); return savedUser ? JSON.parse(savedUser) : { name: '', email: '' }; });
return <div>{/* component JSX */}</div>;}
function expensiveCalculation() { console.log('Running expensive calculation...'); // Simulate expensive operation let result = 0; for (let i = 0; i < 1000000; i++) { result += i; } return result;}Multiple State Variables
You can use multiple useState calls in a single component:
function LoginForm() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { await login(username, password); } catch (err) { setError(err.message); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> <input value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" disabled={isLoading} /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" disabled={isLoading} /> <button type="submit" disabled={isLoading}> {isLoading ? 'Logging in...' : 'Login'} </button> {error && <p style={{ color: 'red' }}>{error}</p>} </form> );}Common Patterns
Toggle Boolean State
function ToggleExample() { const [isVisible, setIsVisible] = useState(false);
// Simple toggle const toggle = () => setIsVisible(!isVisible);
// Or using functional update const toggleFunctional = () => setIsVisible(prev => !prev);
return ( <div> <button onClick={toggle}> {isVisible ? 'Hide' : 'Show'} </button> {isVisible && <p>Now you see me!</p>} </div> );}Form State Management
function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '', message: '' });
const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); };
const handleSubmit = (e) => { e.preventDefault(); console.log('Form submitted:', formData); // Reset form setFormData({ name: '', email: '', message: '' }); };
return ( <form onSubmit={handleSubmit}> <input name="name" value={formData.name} onChange={handleChange} placeholder="Name" /> <input name="email" type="email" value={formData.email} onChange={handleChange} placeholder="Email" /> <textarea name="message" value={formData.message} onChange={handleChange} placeholder="Message" /> <button type="submit">Send</button> </form> );}Best Practices
- Use multiple state variables for unrelated data instead of one large object
- Use functional updates when new state depends on previous state
- Initialize state with functions for expensive calculations
- Keep state flat when possible to avoid complex update logic
- Consider useReducer for complex state logic with multiple sub-values
Common Mistakes
Mutating State Directly
// ❌ Don't mutate state directlyconst [items, setItems] = useState([1, 2, 3]);items.push(4); // This won't trigger a re-render!
// ✅ Create new arrays/objectssetItems(prev => [...prev, 4]);Forgetting Functional Updates
// ❌ This might not work as expectedconst [count, setCount] = useState(0);const increment = () => setCount(count + 1);
// ✅ Always reliableconst increment = () => setCount(prev => prev + 1);Next Steps
Now that you understand useState, you’re ready to learn about useEffect for handling side effects and lifecycle events in functional components.
The combination of useState and useEffect covers most of what you need for basic React component logic!