useState Hook
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!