React State Management - Complete Guide
State is what makes React components interactive and dynamic. Understanding how to manage state effectively is crucial for building React applications. This guide covers everything you need to know about React state management.
What is State?
State is data that can change over time and affects what gets rendered on the screen. Unlike props, which are read-only and passed from parent components, state is managed within a component and can be updated using the setState function.
When state changes, React automatically re-renders the component to reflect the new data.
useState Hook
The useState hook is the most common way to add state to functional components.
Basic Syntax
import { 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>
);
}The useState hook returns an array with two elements:
- The current state value
- A function to update that value
State with Different Data Types
String State
function NameInput() {
const [name, setName] = useState('');
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Hello, {name || 'Guest'}!</p>
</div>
);
}Boolean State
function Toggle() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle Visibility
</button>
{isVisible && <p>Now you see me!</p>}
</div>
);
}Array State
function TodoList() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, input]);
setInput('');
}
};
return (
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
}Object State
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
age: ''
});
const handleChange = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
return (
<div>
<input
type="text"
placeholder="Name"
value={user.name}
onChange={(e) => handleChange('name', e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={user.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
<input
type="number"
placeholder="Age"
value={user.age}
onChange={(e) => handleChange('age', e.target.value)}
/>
<div>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<p>Age: {user.age}</p>
</div>
</div>
);
}State Update Patterns
Functional Updates
When the new state depends on the previous state, use functional updates:
function Counter() {
const [count, setCount] = useState(0);
// Bad - can cause issues with batching
const incrementBad = () => {
setCount(count + 1);
setCount(count + 1);
};
// Good - uses functional update
const incrementGood = () => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementGood}>Increment</button>
</div>
);
}Updating Arrays
Never mutate arrays directly. Always create new arrays:
function ArrayExample() {
const [items, setItems] = useState(['apple', 'banana']);
// Add item
const addItem = (newItem) => {
setItems([...items, newItem]);
};
// Remove item
const removeItem = (index) => {
setItems(items.filter((_, i) => i !== index));
};
// Update item
const updateItem = (index, newValue) => {
setItems(items.map((item, i) =>
i === index ? newValue : item
));
};
return (
<div>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<button onClick={() => removeItem(index)}>Remove</button>
</li>
))}
</ul>
<button onClick={() => addItem('orange')}>Add Orange</button>
</div>
);
}Updating Objects
Never mutate objects directly. Always create new objects:
function ObjectExample() {
const [user, setUser] = useState({
name: 'John',
address: {
street: '123 Main St',
city: 'New York'
}
});
// Update top-level property
const updateName = (newName) => {
setUser(prevUser => ({
...prevUser,
name: newName
}));
};
// Update nested property
const updateCity = (newCity) => {
setUser(prevUser => ({
...prevUser,
address: {
...prevUser.address,
city: newCity
}
}));
};
return (
<div>
<p>Name: {user.name}</p>
<p>City: {user.address.city}</p>
<button onClick={() => updateName('Jane')}>Change Name</button>
<button onClick={() => updateCity('Boston')}>Change City</button>
</div>
);
}Multiple State Variables
You can use multiple useState hooks in one component:
function Form() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Form submitted:', { email, password });
} catch (err) {
setError('Submission failed');
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={isLoading}>
{isLoading ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}State vs Props
Understanding when to use state vs props is important:
// Parent component manages state
function ParentComponent() {
const [message, setMessage] = useState('Hello from parent');
return (
<div>
<ChildComponent message={message} />
<button onClick={() => setMessage('New message')}>
Update Message
</button>
</div>
);
}
// Child component receives data via props
function ChildComponent({ message }) {
// Child cannot modify props directly
return <p>{message}</p>;
}Lifting State Up
When multiple components need to share state, lift it up to their common ancestor:
function TemperatureConverter() {
const [celsius, setCelsius] = useState('');
const [fahrenheit, setFahrenheit] = useState('');
const handleCelsiusChange = (value) => {
setCelsius(value);
if (value === '') {
setFahrenheit('');
} else {
const f = (parseFloat(value) * 9/5) + 32;
setFahrenheit(f.toFixed(2));
}
};
const handleFahrenheitChange = (value) => {
setFahrenheit(value);
if (value === '') {
setCelsius('');
} else {
const c = (parseFloat(value) - 32) * 5/9;
setCelsius(c.toFixed(2));
}
};
return (
<div>
<CelsiusInput
value={celsius}
onChange={handleCelsiusChange}
/>
<FahrenheitInput
value={fahrenheit}
onChange={handleFahrenheitChange}
/>
</div>
);
}
function CelsiusInput({ value, onChange }) {
return (
<div>
<label>Celsius:</label>
<input
type="number"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
}
function FahrenheitInput({ value, onChange }) {
return (
<div>
<label>Fahrenheit:</label>
<input
type="number"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
}State Management Best Practices
1. Keep State Minimal
Only store in state what you need for rendering:
// Bad - storing derived data
function UserComponent() {
const [user, setUser] = useState({ name: 'John', age: 25 });
const [displayName, setDisplayName] = useState(''); // Derived!
useEffect(() => {
setDisplayName(`${user.name} (${user.age})`);
}, [user]);
return <p>{displayName}</p>;
}
// Good - derive when needed
function UserComponent() {
const [user, setUser] = useState({ name: 'John', age: 25 });
const displayName = `${user.name} (${user.age})`; // Derived on render
return <p>{displayName}</p>;
}2. Use Multiple State Variables for Unrelated Data
// Good - separate concerns
function UserProfile() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isLoading, setIsLoading] = useState(false);
// ...
}3. Group Related State
// Good - related data together
function FormComponent() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const [formStatus, setFormStatus] = useState({
isLoading: false,
error: '',
success: false
});
// ...
}Common State Management Patterns
1. Form State Pattern
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (onSubmit) => {
setIsSubmitting(true);
setErrors({});
try {
await onSubmit(values);
} catch (error) {
setErrors(error);
} finally {
setIsSubmitting(false);
}
};
return {
values,
errors,
isSubmitting,
handleChange,
handleSubmit
};
}
function ContactForm() {
const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm({
name: '',
email: '',
message: ''
});
const onSubmit = async (formData) => {
// Submit form data
console.log('Submitting:', formData);
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(onSubmit);
}}>
<input
name="name"
value={values.name}
onChange={handleChange}
placeholder="Name"
/>
{errors.name && <span className="error">{errors.name}</span>}
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
<textarea
name="message"
value={values.message}
onChange={handleChange}
placeholder="Message"
/>
{errors.message && <span className="error">{errors.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send'}
</button>
</form>
);
}2. Toggle Pattern
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(prev => !prev);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, { toggle, setTrue, setFalse }];
}
function Modal() {
const [isOpen, { toggle, setTrue, setFalse }] = useToggle(false);
return (
<div>
<button onClick={toggle}>Open Modal</button>
{isOpen && (
<div className="modal">
<div className="modal-content">
<h2>Modal Title</h2>
<p>Modal content goes here.</p>
<button onClick={setFalse}>Close</button>
</div>
</div>
)}
</div>
);
}3. Async Data Pattern
function useAsync(asyncFunction, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
setLoading(true);
const result = await asyncFunction();
if (!isCancelled) {
setData(result);
setError(null);
}
} catch (err) {
if (!isCancelled) {
setError(err);
setData(null);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
isCancelled = true;
};
}, dependencies);
return { data, loading, error };
}
function UserList() {
const { data: users, loading, error } = useAsync(
() => fetch('/api/users').then(res => res.json()),
[]
);
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}When to Consider State Management Libraries
While React’s built-in state management is great for most applications, you might need external libraries for:
- Complex state logic - When state updates become complex
- Shared state - When many components need the same state
- Performance optimization - When you need fine-grained control over re-renders
- Time-travel debugging - When you need to track state changes
Popular options include:
- Redux - Predictable state container
- Zustand - Minimal state management
- Recoil - Facebook’s experimental state management
- Context API - React’s built-in solution for global state
Summary
State management is a core React concept. Remember these key points:
- Use
useStatefor local component state - Never mutate state directly - always create new objects/arrays
- Use functional updates when new state depends on previous state
- Lift state up when components need to share data
- Keep state minimal and derive values when possible
- Group related state and separate unrelated concerns
- Consider custom hooks for reusable state logic
Mastering state management will help you build more maintainable and scalable React applications.
External Resources:
- React Official Docs - State and Lifecycle
- React Hooks Reference - useState
- A Complete Guide to useState
Related Tutorials: