
Introduction
State management is at the heart of React applications. The proper management of state is crucial for building predictable, maintainable, and performant applications. In this chapter, we’ll explore the fundamentals of state management in React, from local component state to more complex state management patterns.
Understanding State
State in React represents the data that can change over time and affects what’s rendered to the screen. Unlike props, which are passed from parent to child components, state is managed within a component.
Characteristics of State
- Mutable: State can be updated over time
- Private: State is fully controlled by the component that owns it
- Local: Changes to state only affect the component that owns it and its children
- Asynchronous: State updates may be batched for performance reasons
Local State Management
Class Components State
In class components, state is defined as a property of the class:
class Counter extends React.Component {
constructor(props) {
super(props);
// Initialize state
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
Updating State in Class Components
State should never be modified directly. Instead, use setState():
// ❌ Don't do this
this.state.count = this.state.count + 1;
// ✅ Do this instead
this.setState({ count: this.state.count + 1 });
Asynchronous Updates
setState() operations are asynchronous. When you need to update state based on the previous state, use the functional form of setState():
// ❌ May lead to incorrect state
this.setState({ count: this.state.count + 1 });
// ✅ Guarantees correct state
this.setState(prevState => ({
count: prevState.count + 1
}));
State Updates with Props
When state depends on both previous state and props, you can access both in the updater function:
this.setState((prevState, props) => ({
count: prevState.count + props.increment
}));
Functional Components with useState Hook
In functional components, local state is managed using the useState hook:
import React, { useState } from 'react';
function Counter() {
// Initialize state with useState hook
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Working with Complex State
Object State
When your state is an object, you need to merge the previous state manually:
const [user, setUser] = useState({
name: 'John',
email: 'john@example.com'
});
// Update only the email
setUser(prevUser => ({
...prevUser,
email: 'new-email@example.com'
}));
Array State
For arrays, you can use array methods like map, filter, and the spread operator:
const [items, setItems] = useState([1, 2, 3]);
// Add an item
setItems(prevItems => [...prevItems, 4]);
// Remove an item
setItems(prevItems => prevItems.filter(item => item !== 2));
// Update an item
setItems(prevItems => prevItems.map(item => item === 2 ? 20 : item));
Component Communication Patterns
Parent to Child Communication
The most common pattern is passing props from parent to child:
function Parent() {
const [message, setMessage] = useState('Hello from parent');
return <Child message={message} />;
}
function Child({ message }) {
return <p>{message}</p>;
}
Child to Parent Communication
Children can communicate with parents through callback functions:
function Parent() {
const [message, setMessage] = useState('Hello');
const handleMessageChange = (newMessage) => {
setMessage(newMessage);
};
return (
<div>
<p>Message: {message}</p>
<Child onMessageChange={handleMessageChange} />
</div>
);
}
function Child({ onMessageChange }) {
return (
<button onClick={() => onMessageChange('Hello from child')}>
Change Message
</button>
);
}
Sibling Communication
Sibling components can communicate through a shared parent:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<SiblingA count={count} />
<SiblingB onIncrement={() => setCount(count + 1)} />
</div>
);
}
function SiblingA({ count }) {
return <p>Count: {count}</p>;
}
function SiblingB({ onIncrement }) {
return <button onClick={onIncrement}>Increment</button>;
}
State Lifting
When multiple components need to share state, you can lift the state up to their closest common ancestor:
function TemperatureCalculator() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
const handleCelsiusChange = (temperature) => {
setScale('c');
setTemperature(temperature);
};
const handleFahrenheitChange = (temperature) => {
setScale('f');
setTemperature(temperature);
};
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
In this example, TemperatureCalculator lifts the state up and coordinates the synchronization between TemperatureInput components.
State Colocation
A key principle of React state management is keeping state as close as possible to where it’s used:
// ❌ Keeping state too high
function App() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
return (
<div>
<Header />
<UserForm firstName={firstName} lastName={lastName}
onFirstNameChange={setFirstName} onLastNameChange={setLastName} />
<Footer />
</div>
);
}
// ✅ Keeping state colocated
function App() {
return (
<div>
<Header />
<UserForm />
<Footer />
</div>
);
}
function UserForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// Form logic here
}
Local vs. Global State
Understanding when to use local versus global state is crucial:
Local State
Use local state when:
- The state is only needed by a single component or a small part of the application
- The state doesn’t need to persist across component unmounts
- The state is specific to a single UI element or interaction
Global State
Consider global state when:
- The state needs to be accessed by many components across the application
- The state needs to persist across component unmounts
- The state represents application-wide data
State Management Patterns
Container/Presentational Pattern
Separate components into two types:
- Container components: Manage state and logic
- Presentational components: Render UI based on props
// Container component
function UserContainer() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser()
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <LoadingIndicator />;
if (error) return <ErrorMessage error={error} />;
return <UserProfile user={user} />;
}
// Presentational component
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Compound Components Pattern
Create components that work together and share state implicitly:
function Accordion() {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div>
<AccordionItem
title="Item 1"
isActive={activeIndex === 0}
onClick={() => setActiveIndex(0)}
>
Content for item 1
</AccordionItem>
<AccordionItem
title="Item 2"
isActive={activeIndex === 1}
onClick={() => setActiveIndex(1)}
>
Content for item 2
</AccordionItem>
</div>
);
}
function AccordionItem({ title, isActive, onClick, children }) {
return (
<div>
<button onClick={onClick}>{title}</button>
{isActive && <div>{children}</div>}
</div>
);
}
Render Props Pattern
Pass a function as a prop to share state logic:
function Toggle(props) {
const [on, setOn] = useState(false);
const toggle = () => setOn(!on);
return props.render({
on,
toggle,
});
}
function App() {
return (
<Toggle
render={({ on, toggle }) => (
<div>
<button onClick={toggle}>Toggle</button>
{on && <p>Content visible when toggled on</p>}
</div>
)}
/>
);
}
Custom Hooks Pattern
Extract and reuse stateful logic:
// Custom hook
function useToggle(initialState = false) {
const [on, setOn] = useState(initialState);
const toggle = () => setOn(!on);
return [on, toggle];
}
// Usage
function ToggleButton() {
const [on, toggle] = useToggle();
return (
<button onClick={toggle}>
{on ? 'ON' : 'OFF'}
</button>
);
}
State Management Best Practices
1. Keep State Minimal
Only store values in state that are actually needed for rendering or data flow:
// ❌ Derived state
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('John Doe');
// When updating first name
setFirstName(newFirstName);
setFullName(`${newFirstName} ${lastName}`);
// ✅ Compute derived values on render
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
// Compute full name during render
const fullName = `${firstName} ${lastName}`;
2. Use Function Updates for State That Depends on Previous State
// ❌ May lead to incorrect state
setCount(count + 1);
// ✅ Guarantees correct state
setCount(prevCount => prevCount + 1);
3. Batch State Updates
React may batch multiple state updates for performance. When possible, combine related state changes:
// ❌ Multiple state updates
function handleSubmit() {
setSubmitting(true);
setFormErrors({});
setSuccess(false);
}
// ✅ Combined into a single object
function handleFormState(state) {
setFormState(prevState => ({
...prevState,
submitting: true,
errors: {},
success: false
}));
}
4. Avoid State Duplication
Don’t store the same data in multiple places:
// ❌ Duplicated state
const [users, setUsers] = useState([]);
const [userCount, setUserCount] = useState(0);
// When updating users
setUsers(newUsers);
setUserCount(newUsers.length);
// ✅ Derive values from state
const [users, setUsers] = useState([]);
const userCount = users.length;
5. Optimize State Location
Keep state as close as possible to where it’s used:
// ❌ State too high in component tree
function App() {
const [searchTerm, setSearchTerm] = useState('');
return (
<div>
<Header />
<Sidebar />
<MainContent>
<SearchBox
value={searchTerm}
onChange={setSearchTerm}
/>
<SearchResults searchTerm={searchTerm} />
</MainContent>
</div>
);
}
// ✅ State colocated with related components
function App() {
return (
<div>
<Header />
<Sidebar />
<MainContent>
<SearchFeature />
</MainContent>
</div>
);
}
function SearchFeature() {
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<SearchBox
value={searchTerm}
onChange={setSearchTerm}
/>
<SearchResults searchTerm={searchTerm} />
</>
);
}
Debugging State Issues
Common State Problems
- State not updating immediately: Remember that state updates are asynchronous
- Incorrect state value: Ensure you’re using the functional form of setState when updating based on previous state
- State updates not reflected in UI: Check that you’re actually setting state with the setter function, not modifying the state directly
- Stale closures: Be aware of closures capturing old state values in event handlers or effects
State Debugging Tools
- React DevTools: The Components tab shows current state values
- Console logging: Log state changes to understand when and how they occur
- useEffect with dependencies: Track state changes with effects
function DebugComponent() {
const [count, setCount] = useState(0);
// Log state changes
useEffect(() => {
console.log('Count changed:', count);
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
Increment
</button>
);
}
When to Use More Advanced State Management
As your application grows, you might need more advanced state management solutions:
Signs you need more advanced state management:
- Deep prop drilling: Passing props through many layers of components
- Complex state logic: State updates that require multiple steps or conditions
- Global state needs: Data that needs to be accessed anywhere in the app
- Persistence requirements: State that needs to be saved and restored
In these situations, you might consider:
- React Context API (covered in Chapter 15)
- Redux (covered in Chapter 18)
- MobX (covered in Chapter 19)
- Zustand, Recoil, Jotai, or other lightweight state management libraries
Practical Example: Shopping Cart
Let’s put it all together with a practical example of a shopping cart using state management techniques:
import React, { useState } from 'react';
// Product data
const products = [
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 20 },
{ id: 3, name: 'Product 3', price: 30 },
];
function ShoppingCart() {
// Cart state
const [cart, setCart] = useState([]);
// Add item to cart
const addToCart = (product) => {
setCart(prevCart => {
// Check if product already in cart
const existingItem = prevCart.find(item => item.id === product.id);
if (existingItem) {
// Update quantity if product already in cart
return prevCart.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
// Add new product to cart
return [...prevCart, { ...product, quantity: 1 }];
}
});
};
// Remove item from cart
const removeFromCart = (productId) => {
setCart(prevCart => prevCart.filter(item => item.id !== productId));
};
// Update item quantity
const updateQuantity = (productId, newQuantity) => {
if (newQuantity < 1) {
removeFromCart(productId);
return;
}
setCart(prevCart =>
prevCart.map(item =>
item.id === productId
? { ...item, quantity: newQuantity }
: item
)
);
};
// Calculate total price
const totalPrice = cart.reduce(
(total, item) => total + item.price * item.quantity,
0
);
return (
<div className="shopping-cart">
<h1>Shopping Cart</h1>
<div className="product-list">
<h2>Products</h2>
{products.map(product => (
<div key={product.id} className="product">
<span>{product.name} - ${product.price}</span>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
<div className="cart">
<h2>Your Cart</h2>
{cart.length === 0 ? (
<p>Your cart is empty</p>
) : (
<>
{cart.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name} - ${item.price} x {item.quantity}</span>
<div className="quantity-controls">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
</div>
<button onClick={() => removeFromCart(item.id)}>Remove</button>
</div>
))}
<div className="cart-total">
<strong>Total: ${totalPrice}</strong>
</div>
</>
)}
</div>
</div>
);
}
This example demonstrates:
- Managing complex state (shopping cart items with quantities)
- Updating state based on previous state
- Deriving values from state (total price)
- Handling multiple operations on the same state
Summary
State management is fundamental to building React applications. We’ve covered:
- The basics of component state in class and functional components
- Working with complex state objects and arrays
- Patterns for component communication
- State lifting and colocation
- Local vs global state considerations
- Common state management patterns
- Best practices for managing state
- Debugging state issues
- When to consider more advanced state management solutions
With these fundamentals, you’ll be well-equipped to handle state management in simple to moderately complex React applications. In the next chapter, we’ll explore React Hooks, which revolutionized state management and side effects in functional components.