State management is fundamental to building interactive React applications. Here’s a comprehensive guide to handling state in both functional and class components.
1. State in Functional Components (with Hooks)
useState Hook
The primary way to manage local component state:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initialize with 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Key Points:
- Returns a stateful value (
count
) and updater function (setCount
) - State updates trigger re-renders
- Can be used multiple times in one component
Updating State Based on Previous State
setCount(prevCount => prevCount + 1);
Complex State (Objects/Arrays)
const [user, setUser] = useState({
name: 'Alice',
age: 25
});
// Update correctly (create new object)
setUser(prev => ({ ...prev, age: 26 }));
2. State in Class Components
Initializing State
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
}
Updating State Correctly
// Correct - use function when new state depends on previous
this.setState(prevState => ({
count: prevState.count + 1
}));
// Correct - pass object for simple updates
this.setState({ count: 42 });
3. Lifting State Up
When multiple components need to share state:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<ChildA count={count} />
<ChildB setCount={setCount} />
</>
);
}
function ChildA({ count }) {
return <div>Current count: {count}</div>;
}
function ChildB({ setCount }) {
return <button onClick={() => setCount(c => c + 1)}>Increment</button>;
}
4. Common State Patterns
Form Handling
function Form() {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
return (
<form>
<input
name="username"
value={formData.username}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
</form>
);
}
Toggle State
const [isOpen, setIsOpen] = useState(false);
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? 'Close' : 'Open'}
</button>
5. State Management Solutions
Solution | Best For | Description |
---|---|---|
useState | Local component state | Built-in React hook |
useReducer | Complex state logic | Like Redux for components |
Context API | App-wide state | Avoids prop drilling |
Redux | Large applications | Centralized state management |
Zustand | Simpler alternative to Redux | Lightweight state management |
6. Best Practices
- Keep state minimal – Only store what affects UI
- Colocate state – Keep state close to where it’s used
- Avoid deeply nested state – Flatten when possible
- Use proper update patterns – Especially with arrays/objects
- Consider derived state – Compute values from state when possible
7. Common Mistakes
Mutating State Directly
// Wrong ❌
this.state.count = 42;
// Correct ✅
this.setState({ count: 42 });
Forgetting Functional Updates
// Problematic with rapid updates
setCount(count + 1);
// Better
setCount(prev => prev + 1);
State Management Cheat Sheet
// Functional component
const [state, setState] = useState(initialValue);
// Class component
this.state = { key: value };
this.setState({ key: newValue });
// Context for global state
const value = useContext(MyContext);
// useReducer for complex state
const [state, dispatch] = useReducer(reducer, initialState);