State Management: Redux, Zustand, Context API
State management is crucial for scalable React applications. This article covers popular state management solutions.
Introduction
State management provides:
- Centralized state
- Predictable updates
- Time-travel debugging
- Middleware support
- Scalability
Understanding state management helps you:
- Manage complex state
- Share state across components
- Debug state changes
- Scale applications
- Improve performance
Context API
Basic Context Setup
import { createContext, useContext, useState } from 'react';
// โ
Good: Create context
const AppContext = createContext();
// โ
Good: Provider component
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const login = (userData) => {
setUser(userData);
};
const logout = () => {
setUser(null);
};
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
const addNotification = (message) => {
setNotifications([...notifications, message]);
};
const value = {
user,
login,
logout,
theme,
toggleTheme,
notifications,
addNotification
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}
// โ
Good: Custom hook for context
function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
}
// Usage
function App() {
return (
<AppProvider>
<Dashboard />
</AppProvider>
);
}
function Dashboard() {
const { user, logout, theme } = useApp();
return (
<div className={`theme-${theme}`}>
<h1>Welcome, {user?.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
Advanced Context Patterns
// โ
Good: Separate contexts for different concerns
const AuthContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (email, password) => {
setLoading(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const data = await response.json();
setUser(data);
} finally {
setLoading(false);
}
};
return (
<AuthContext.Provider value={{ user, login, loading }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
return useContext(AuthContext);
}
// โ
Good: Reducer pattern with context
const initialState = {
items: [],
loading: false,
error: null
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload]
};
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
}
const CartContext = createContext();
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (itemId) => {
dispatch({ type: 'REMOVE_ITEM', payload: itemId });
};
return (
<CartContext.Provider value={{ state, addItem, removeItem }}>
{children}
</CartContext.Provider>
);
}
function useCart() {
return useContext(CartContext);
}
Redux
Redux Setup
import { createStore, combineReducers } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
// โ
Good: Action types
const ACTIONS = {
LOGIN: 'LOGIN',
LOGOUT: 'LOGOUT',
SET_LOADING: 'SET_LOADING',
SET_ERROR: 'SET_ERROR'
};
// โ
Good: Action creators
const login = (user) => ({
type: ACTIONS.LOGIN,
payload: user
});
const logout = () => ({
type: ACTIONS.LOGOUT
});
const setLoading = (loading) => ({
type: ACTIONS.SET_LOADING,
payload: loading
});
// โ
Good: Reducers
const authReducer = (state = { user: null, loading: false }, action) => {
switch (action.type) {
case ACTIONS.LOGIN:
return { ...state, user: action.payload };
case ACTIONS.LOGOUT:
return { ...state, user: null };
case ACTIONS.SET_LOADING:
return { ...state, loading: action.payload };
default:
return state;
}
};
const themeReducer = (state = 'light', action) => {
switch (action.type) {
case 'TOGGLE_THEME':
return state === 'light' ? 'dark' : 'light';
default:
return state;
}
};
// โ
Good: Combine reducers
const rootReducer = combineReducers({
auth: authReducer,
theme: themeReducer
});
// โ
Good: Create store
const store = createStore(rootReducer);
// โ
Good: Setup app
function App() {
return (
<Provider store={store}>
<Dashboard />
</Provider>
);
}
// โ
Good: Use Redux in components
function Dashboard() {
const dispatch = useDispatch();
const { user, loading } = useSelector(state => state.auth);
const theme = useSelector(state => state.theme);
const handleLogin = async (email, password) => {
dispatch(setLoading(true));
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const data = await response.json();
dispatch(login(data));
} finally {
dispatch(setLoading(false));
}
};
return (
<div className={`theme-${theme}`}>
{loading ? (
<p>Loading...</p>
) : user ? (
<h1>Welcome, {user.name}</h1>
) : (
<button onClick={() => handleLogin('[email protected]', 'password')}>
Login
</button>
)}
</div>
);
}
Redux Middleware
// โ
Good: Custom middleware
const loggerMiddleware = store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('New state:', store.getState());
return result;
};
// โ
Good: Async middleware
const asyncMiddleware = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};
// โ
Good: Create store with middleware
import { applyMiddleware } from 'redux';
const store = createStore(
rootReducer,
applyMiddleware(loggerMiddleware, asyncMiddleware)
);
// โ
Good: Async action creators
const loginAsync = (email, password) => async (dispatch) => {
dispatch(setLoading(true));
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const data = await response.json();
dispatch(login(data));
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
} finally {
dispatch(setLoading(false));
}
};
Zustand
Basic Zustand Setup
import { create } from 'zustand';
// โ
Good: Create store
const useAuthStore = create((set) => ({
user: null,
loading: false,
error: null,
login: async (email, password) => {
set({ loading: true });
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const data = await response.json();
set({ user: data, error: null });
} catch (error) {
set({ error: error.message });
} finally {
set({ loading: false });
}
},
logout: () => {
set({ user: null });
},
setError: (error) => {
set({ error });
}
}));
// โ
Good: Use store in components
function Dashboard() {
const { user, loading, login, logout } = useAuthStore();
return (
<div>
{loading ? (
<p>Loading...</p>
) : user ? (
<>
<h1>Welcome, {user.name}</h1>
<button onClick={logout}>Logout</button>
</>
) : (
<button onClick={() => login('[email protected]', 'password')}>
Login
</button>
)}
</div>
);
}
// โ
Good: Multiple stores
const useThemeStore = create((set) => ({
theme: 'light',
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
}))
}));
const useNotificationStore = create((set) => ({
notifications: [],
addNotification: (message) => set((state) => ({
notifications: [...state.notifications, message]
})),
removeNotification: (id) => set((state) => ({
notifications: state.notifications.filter(n => n.id !== id)
}))
}));
Advanced Zustand Patterns
// โ
Good: Selectors for performance
const useAuthStore = create((set) => ({
user: null,
email: '',
// ...
}));
// Selectors
const selectUser = (state) => state.user;
const selectEmail = (state) => state.email;
function Component() {
const user = useAuthStore(selectUser);
const email = useAuthStore(selectEmail);
// Only re-renders when user or email changes
}
// โ
Good: Middleware
const useStore = create(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}),
(set) => ({
// Middleware
set: (state, action) => {
console.log('State update:', action);
set(state);
}
})
);
// โ
Good: Persist middleware
import { persist } from 'zustand/middleware';
const useAuthStore = create(
persist(
(set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null })
}),
{
name: 'auth-storage'
}
)
);
// โ
Good: Immer middleware
import { immer } from 'zustand/middleware/immer';
const useStore = create(
immer((set) => ({
todos: [],
addTodo: (text) => set((state) => {
state.todos.push({ id: Date.now(), text });
})
}))
);
Comparison
Context API vs Redux vs Zustand
// Context API: Simple, built-in, good for small apps
// Pros: No dependencies, simple API
// Cons: Can cause unnecessary re-renders, verbose
// Redux: Powerful, predictable, great for large apps
// Pros: Time-travel debugging, middleware, large ecosystem
// Cons: Boilerplate, steep learning curve
// Zustand: Simple, lightweight, good for medium apps
// Pros: Minimal boilerplate, good performance, TypeScript support
// Cons: Smaller ecosystem, less mature
// โ
Good: Choose based on needs
// Small app: Context API
// Medium app: Zustand
// Large app: Redux
Best Practices
-
Keep state normalized:
// โ Good: Normalized state const state = { users: { 1: { id: 1, name: 'John' }, 2: { id: 2, name: 'Jane' } }, userIds: [1, 2] }; // โ Bad: Nested state const state = { users: [ { id: 1, name: 'John', posts: [...] }, { id: 2, name: 'Jane', posts: [...] } ] }; -
Use selectors:
// โ Good: Selectors prevent re-renders const selectUserName = (state) => state.user.name; const name = useAuthStore(selectUserName); // โ Bad: Entire state const { user } = useAuthStore(); -
Avoid deeply nested state:
// โ Good: Flat structure const state = { user: { id: 1, name: 'John' }, theme: 'light' }; // โ Bad: Deeply nested const state = { app: { ui: { theme: { current: 'light' } } } };
Summary
State management is essential. Key takeaways:
- Use Context API for simple apps
- Use Zustand for medium apps
- Use Redux for large apps
- Keep state normalized
- Use selectors for performance
- Avoid prop drilling
- Choose based on complexity
Related Resources
- React Context API
- Redux Documentation
- Zustand Documentation
- State Management Patterns
- Redux DevTools
Next Steps
- Learn about React Router
- Explore Form Handling
- Study Performance Optimization
- Practice state management
- Build scalable React applications
Comments