Understanding State Management
As React applications grow, managing state becomes more complex. You need to decide where to store state, how to share it between components, and how to keep it synchronized. This page covers various approaches to state management in React applications.
🤔 When Do You Need State Management?
- Prop Drilling: Passing props through many component levels
- Shared State: Multiple components need the same data
- Complex Updates: State changes affect multiple parts of the app
- Global Features: User authentication, themes, language settings
Local vs Global State
✅ Use Local State For:
- Form input values
- Toggle states (show/hide)
- Component-specific data
- Temporary UI states
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
// Validate and submit
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? 'Hide' : 'Show'} Password
</button>
<button type="submit">Login</button>
</form>
);
}
🌐 Use Global State For:
- User authentication
- Theme preferences
- Shopping cart contents
- App-wide settings
// Context for global state
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
const value = {
user,
setUser,
theme,
setTheme,
cart,
setCart
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
// Any component can access global state
function Header() {
const { user, theme, cart } = useContext(AppContext);
return (
<header className={theme}>
{user ? `Welcome ${user.name}` : 'Please login'}
<CartIcon count={cart.length} />
</header>
);
}
Context API - React's Built-in Solution
React's Context API provides a way to pass data through the component tree without having to pass props down manually at every level. It's perfect for global state like themes, user authentication, and app settings.
🏗️ Building a Complete Context System
// 1. Create Context and Custom Hook
import { createContext, useContext, useReducer } from 'react';
const AppContext = createContext();
// Custom hook to use the context
export const useApp = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
};
// 2. Reducer for complex state logic
const appReducer = (state, action) => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'ADD_TO_CART':
return {
...state,
cart: [...state.cart, action.payload]
};
case 'REMOVE_FROM_CART':
return {
...state,
cart: state.cart.filter(item => item.id !== action.payload)
};
case 'CLEAR_CART':
return { ...state, cart: [] };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
};
// 3. Provider Component
export function AppProvider({ children }) {
const initialState = {
user: null,
theme: 'light',
cart: [],
loading: false,
error: null
};
const [state, dispatch] = useReducer(appReducer, initialState);
// Action creators
const actions = {
setUser: (user) => dispatch({ type: 'SET_USER', payload: user }),
setTheme: (theme) => dispatch({ type: 'SET_THEME', payload: theme }),
addToCart: (item) => dispatch({ type: 'ADD_TO_CART', payload: item }),
removeFromCart: (id) => dispatch({ type: 'REMOVE_FROM_CART', payload: id }),
clearCart: () => dispatch({ type: 'CLEAR_CART' }),
setLoading: (loading) => dispatch({ type: 'SET_LOADING', payload: loading }),
setError: (error) => dispatch({ type: 'SET_ERROR', payload: error })
};
const value = {
...state,
...actions
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
// 4. Using the Context in Components
function Header() {
const { user, theme, cart, setTheme } = useApp();
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<header className={`header ${theme}`}>
<h1>My Store</h1>
{user ? (
<div>Welcome, {user.name}!</div>
) : (
<LoginButton />
)}
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
<CartIcon count={cart.length} />
</header>
);
}
function ProductCard({ product }) {
const { addToCart } = useApp();
const handleAddToCart = () => {
addToCart({
id: product.id,
name: product.name,
price: product.price,
quantity: 1
});
};
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={handleAddToCart}>
Add to Cart
</button>
</div>
);
}
// 5. App Setup
function App() {
return (
<AppProvider>
<Header />
<ProductList />
<Cart />
</AppProvider>
);
}
Redux - Predictable State Container
Redux is a popular state management library that implements a unidirectional data flow pattern. While not always necessary, Redux shines in large applications with complex state interactions.
🔄 Redux Core Concepts
🏪 Store
Single source of truth that holds the entire state of your application.
📋 Actions
Plain objects that describe what happened. They have a type and optional payload.
🔧 Reducers
Pure functions that specify how state changes in response to actions.
📡 Dispatch
Function used to send actions to the store to trigger state changes.
// 1. Actions - What happened
const actions = {
increment: () => ({ type: 'INCREMENT' }),
decrement: () => ({ type: 'DECREMENT' }),
setCount: (count) => ({ type: 'SET_COUNT', payload: count }),
addTodo: (text) => ({
type: 'ADD_TODO',
payload: { id: Date.now(), text, completed: false }
})
};
// 2. Reducers - How state changes
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_COUNT':
return { ...state, count: action.payload };
default:
return state;
}
};
const todosReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
};
// 3. Combine Reducers
const rootReducer = combineReducers({
counter: counterReducer,
todos: todosReducer
});
// 4. Create Store
const store = createStore(rootReducer);
// 5. Usage in Components (with React-Redux)
function Counter() {
const count = useSelector(state => state.counter.count);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(actions.increment())}>
+
</button>
<button onClick={() => dispatch(actions.decrement())}>
-
</button>
</div>
);
}
Modern State Management Alternatives
🔮 Zustand
Small, fast, and scalable state management solution.
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
function Counter() {
const { count, increment, decrement } = useStore()
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
⚛️ Jotai
Atomic approach to React state management.
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>
+
</button>
</div>
)
}
🌊 Valtio
Proxy-based state management for React.
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0 })
function Counter() {
const snap = useSnapshot(state)
return (
<div>
<p>{snap.count}</p>
<button onClick={() => ++state.count}>
+
</button>
</div>
)
}
📊 Redux Toolkit
Modern Redux with less boilerplate.
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
}
}
})
export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
Choosing the Right State Management Solution
🤔 Decision Matrix
✅ Use:
- useState for local component state
- useContext for 2-3 global values
- Props for parent-child communication
📝 Characteristics:
- Few shared state values
- Simple data flow
- Small development team
✅ Consider:
- Redux Toolkit for predictable updates
- Zustand for simplicity + power
- Jotai for atomic state
📝 Characteristics:
- Complex state interactions
- Time-travel debugging needs
- Large development team
State Management Best Practices
✅ General Guidelines
- Start with local state
- Lift state up when needed
- Use Context for truly global state
- Keep state as close to usage as possible
- Normalize complex state structures
🏗️ Architecture Tips
- Separate UI state from server state
- Use custom hooks for state logic
- Implement error boundaries
- Consider using React Query for server state
- Keep actions and reducers pure
⚡ Performance
- Avoid large Context values
- Split Context by update frequency
- Use selectors in Redux
- Memoize expensive calculations
- Implement proper loading states
🐛 Testing
- Test reducers in isolation
- Mock Context providers
- Test integration with components
- Use testing utilities like @testing-library
- Test error scenarios
What's Next?
You now understand the various approaches to state management in React applications. Next, you'll learn about routing to build single-page applications with multiple views.
🚀 Continue Learning
- Routing - Building single-page applications with React Router
- Development Tools - Essential tools for React development
- Next.js - Full-stack React framework
- Transition Guide - Complete migration strategies