useState Hook
The useState Hook is the most fundamental Hook in React. It allows functional components to have state, which was previously only possible with class components.
Basic Syntax
const [state, setState] = useState(initialState);
state: The current state valuesetState: Function to update the stateinitialState: The initial state value
Simple Examples
Counter
import React, { useState } from 'react';
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>
);
}
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>
);
}
Toggle Boolean
function Toggle() {
const [isOn, setIsOn] = useState(false);
return (
<div>
<button onClick={() => setIsOn(!isOn)}>
{isOn ? 'ON' : 'OFF'}
</button>
<p>The switch is {isOn ? 'on' : 'off'}</p>
</div>
);
}
Different State Types
Primitive Values
function PrimitiveStates() {
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [isVisible, setIsVisible] = useState(true); // boolean
const [data, setData] = useState(null); // null
const [id, setId] = useState(undefined); // undefined
return <div>{/* Use states */}</div>;
}
Object State
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
// Update entire object
const updateUser = (newUser) => {
setUser(newUser);
};
// Update specific field
const updateField = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
return (
<div>
<input
value={user.name}
onChange={(e) => updateField('name', e.target.value)}
placeholder="Name"
/>
<input
value={user.email}
onChange={(e) => updateField('email', e.target.value)}
placeholder="Email"
/>
<input
type="number"
value={user.age}
onChange={(e) => updateField('age', parseInt(e.target.value))}
placeholder="Age"
/>
</div>
);
}
Array State
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 removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add 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 new state depends on previous state, use the functional form:
function Counter() {
const [count, setCount] = useState(0);
// ❌ May not work correctly
const incrementTwiceBad = () => {
setCount(count + 1);
setCount(count + 1); // Still increments by 1!
};
// ✅ Correct way
const incrementTwiceGood = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // Increments by 2
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementTwiceGood}>+2</button>
</div>
);
}
Why Functional Updates?
function DelayedCounter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
// ❌ Uses stale count value
// setCount(count + 1);
// ✅ Always uses current value
setCount(prevCount => prevCount + 1);
}, 3000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment after 3s</button>
</div>
);
}
Lazy Initial State
For expensive computations, use a function to initialize state:
// ❌ Expensive function runs on every render
function ExpensiveComponent() {
const [data, setData] = useState(expensiveComputation());
return <div>{/* ... */}</div>;
}
// ✅ Function only runs once
function ExpensiveComponent() {
const [data, setData] = useState(() => expensiveComputation());
return <div>{/* ... */}</div>;
}
// Real example
function TodoApp() {
const [todos, setTodos] = useState(() => {
// Only runs on first render
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
return <div>{/* ... */}</div>;
}
Multiple State Variables vs Single Object
Multiple State Variables (Recommended)
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
// Easy to update individually
// Clear what each setter does
// Can have different types
}
Single Object State
function Form() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: 0
});
// Must spread to update
const updateField = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
}
State Update Batching
React batches state updates for performance:
function BatchingExample() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
// These updates are batched
setCount(c => c + 1);
setFlag(f => !f);
// Only one re-render happens
};
console.log('Rendered with', { count, flag });
return (
<div>
<button onClick={handleClick}>Update Both</button>
<p>Count: {count}, Flag: {flag.toString()}</p>
</div>
);
}
Common Patterns
Form Handling
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user types
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const validate = () => {
const newErrors = {};
if (!formData.name) newErrors.name = 'Name required';
if (!formData.email) newErrors.email = 'Email required';
if (!formData.message) newErrors.message = 'Message required';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
setIsSubmitting(true);
try {
// Submit form
await submitForm(formData);
// Reset form
setFormData({ name: '', email: '', message: '' });
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
{errors.name && <span>{errors.name}</span>}
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span>{errors.email}</span>}
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
{errors.message && <span>{errors.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
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 result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<button onClick={fetchData}>Fetch Data</button>
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
</div>
);
}
Undo/Redo
function UndoableCounter() {
const [count, setCount] = useState(0);
const [history, setHistory] = useState([0]);
const [currentIndex, setCurrentIndex] = useState(0);
const updateCount = (newCount) => {
const newHistory = history.slice(0, currentIndex + 1);
newHistory.push(newCount);
setHistory(newHistory);
setCurrentIndex(newHistory.length - 1);
setCount(newCount);
};
const undo = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
setCount(history[currentIndex - 1]);
}
};
const redo = () => {
if (currentIndex < history.length - 1) {
setCurrentIndex(currentIndex + 1);
setCount(history[currentIndex + 1]);
}
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => updateCount(count + 1)}>+</button>
<button onClick={() => updateCount(count - 1)}>-</button>
<button onClick={undo} disabled={currentIndex === 0}>Undo</button>
<button onClick={redo} disabled={currentIndex === history.length - 1}>
Redo
</button>
</div>
);
}
Best Practices
- Keep state minimal - Only store what you can't compute
- Lift state up when needed by multiple components
- Use functional updates when depending on previous state
- Don't mutate state - Always create new objects/arrays
- Group related state - But don't overdo it
- Initialize expensive state lazily
- Clear state when components unmount if needed
Common Mistakes
Mutating State
// ❌ Never mutate state directly
const [user, setUser] = useState({ name: 'John', age: 30 });
// Wrong
user.name = 'Jane';
setUser(user);
// ✅ Create new object
setUser({ ...user, name: 'Jane' });
Using State Immediately
// ❌ State updates are asynchronous
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // Still logs old value!
};
// ✅ Use the new value directly
const handleClick = () => {
const newCount = count + 1;
setCount(newCount);
console.log(newCount); // Logs new value
};
The useState Hook is the foundation of state management in functional components. Master it and you'll be able to build any interactive UI!