State & useState
Managing component state with the useState hook
What Is State?
State is data that changes over time in your component. Unlike props (which come from parent components), state is managed within the component itself. When state changes, React automatically re-renders the component to reflect those changes.
Why Do We Need State?
Regular variables don't trigger re-renders when they change. State is React's way of remembering values between renders and updating the UI when those values change.
The useState Hook
useState is a React Hook that lets you add state to functional components:
import { useState } from 'react';
function Counter() {
// Declare a state variable called "count"
// useState returns: [currentValue, setterFunction]
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
useState Syntax Breakdown
const [state, setState] = useState(initialValue);
// state - The current value
// setState - Function to update the value
// initialValue - Starting value (only used on first render)
π Naming Convention
Use descriptive names: [value, setValue], [isOpen, setIsOpen], [users, setUsers]
π Re-rendering
Calling the setter function triggers a re-render with the new value
Different Types of State
import { useState } from 'react';
function Examples() {
// String state
const [name, setName] = useState('');
// Number state
const [count, setCount] = useState(0);
// Boolean state
const [isVisible, setIsVisible] = useState(false);
// Array state
const [items, setItems] = useState([]);
// Object state
const [user, setUser] = useState({ name: '', email: '' });
// null/undefined initial state
const [data, setData] = useState(null);
return <div>...</div>;
}
Updating State Correctly
β οΈ Never Modify State Directly
Always use the setter function. Direct mutation won't trigger a re-render.
function Counter() {
const [count, setCount] = useState(0);
// β WRONG - Direct mutation
const wrongIncrement = () => {
count = count + 1; // This won't work!
};
// β CORRECT - Use setter function
const correctIncrement = () => {
setCount(count + 1);
};
return <button onClick={correctIncrement}>{count}</button>;
}
Functional Updates
When the new state depends on the previous state, use a function:
function Counter() {
const [count, setCount] = useState(0);
// β May cause issues with batched updates
const increment = () => {
setCount(count + 1);
setCount(count + 1); // Still uses the old count!
};
// β CORRECT - Functional update
const incrementCorrect = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Works correctly!
};
// Practical example
const addThree = () => {
setCount(prevCount => prevCount + 3);
};
return <div>{count}</div>;
}
Updating Objects in State
Always create a new object when updating object state:
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
// β WRONG - Mutating existing object
const updateNameWrong = (newName) => {
user.name = newName; // Direct mutation!
setUser(user); // Same reference, no re-render
};
// β CORRECT - Create new object with spread
const updateName = (newName) => {
setUser({
...user, // Copy all existing properties
name: newName // Override the one you're changing
});
};
// Update multiple fields
const updateUser = (updates) => {
setUser(prev => ({
...prev,
...updates
}));
};
return (
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
/>
);
}
Updating Arrays in State
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', done: false }
]);
// Add item
const addTodo = (text) => {
setTodos([
...todos,
{ id: Date.now(), text, done: false }
]);
};
// Remove item
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// Update item
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, done: !todo.done }
: todo
));
};
// Replace all items
const clearAll = () => {
setTodos([]);
};
return <div>...</div>;
}
π‘ Array Methods Reference
- Add:
[...arr, newItem]or[newItem, ...arr] - Remove:
arr.filter(item => item.id !== id) - Update:
arr.map(item => item.id === id ? {...item, ...changes} : item)
Multiple State Variables
function Form() {
// Separate state for each value (recommended for unrelated data)
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState({});
// OR group related state together
const [formData, setFormData] = useState({
name: '',
email: ''
});
const [formState, setFormState] = useState({
isSubmitting: false,
errors: {}
});
return <form>...</form>;
}
Lazy Initial State
For expensive initial values, pass a function to useState:
// β Runs on every render (expensive)
const [data, setData] = useState(expensiveCalculation());
// β Runs only on first render (lazy)
const [data, setData] = useState(() => expensiveCalculation());
// Practical example: Reading from localStorage
function App() {
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('theme');
return saved || 'light';
});
return <div className={theme}>...</div>;
}
Common Patterns
// Toggle boolean
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);
// Counter with min/max
const [count, setCount] = useState(0);
const increment = () => setCount(prev => Math.min(prev + 1, 10));
const decrement = () => setCount(prev => Math.max(prev - 1, 0));
// Reset to initial value
const initialState = { name: '', email: '' };
const [form, setForm] = useState(initialState);
const resetForm = () => setForm(initialState);
π― useState Best Practices
- β Keep state as minimal as possible
- β Use functional updates when new state depends on old state
- β Never mutate state directlyβalways create new objects/arrays
- β Group related state, separate unrelated state
- β Use lazy initialization for expensive computations
- β Lift state up when multiple components need the same data