Learn state management in React including Context API, Redux, Zustand, and other state management solutions.
Learn state management in React including Context API, Redux, Zustand, and other state management solutions.
Learn React Context API for sharing state across components without prop drilling
Content by: Ayush Ladani
React.js Developer
Context API is a React feature that allows you to share state across components without having to explicitly pass props through every level of the component tree.
import React, { createContext, useContext, useState } from 'react';
// Create a context
const ThemeContext = createContext();
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook to use the context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Using the context in components
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'}
</button>
</header>
);
}
// Advanced Context with multiple values
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async (credentials) => {
setLoading(true);
try {
// Simulate API call
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const userData = await response.json();
setUser(userData);
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, loading, login, logout }}>
{children}
</UserContext.Provider>
);
}
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// Using multiple contexts
function App() {
return (
<ThemeProvider>
<UserProvider>
<Header />
<Main />
</UserProvider>
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
const { user, logout } = useUser();
return (
<header className={theme}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'}
</button>
{user && (
<div>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</div>
)}
</header>
);
}
// Exercise: Build an E-commerce App with Context API
// Create a shopping cart system using Context API
// src/contexts/CartContext.js
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
} else {
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
)
};
case 'CLEAR_CART':
return {
...state,
items: []
};
default:
return state;
}
};
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0
});
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (itemId) => {
dispatch({ type: 'REMOVE_ITEM', payload: itemId });
};
const updateQuantity = (itemId, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id: itemId, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const getTotal = () => {
return state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
};
const getItemCount = () => {
return state.items.reduce((count, item) => count + item.quantity, 0);
};
return (
<CartContext.Provider value={{
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
getTotal,
getItemCount
}}>
{children}
</CartContext.Provider>
);
}
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
// src/components/ProductCard.js
function ProductCard({ product }) {
const { addItem } = useCart();
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => addItem(product)}>
Add to Cart
</button>
</div>
);
}
// src/components/Cart.js
function Cart() {
const { items, removeItem, updateQuantity, getTotal, clearCart } = useCart();
if (items.length === 0) {
return <div>Your cart is empty</div>;
}
return (
<div className="cart">
<h2>Shopping Cart</h2>
{items.map(item => (
<div key={item.id} className="cart-item">
<img src={item.image} alt={item.name} />
<div>
<h3>{item.name}</h3>
<p>{item.price}</p>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
min="1"
/>
<button onClick={() => removeItem(item.id)}>
Remove
</button>
</div>
</div>
))}
<div className="cart-total">
<strong>Total: {getTotal()}</strong>
</div>
<button onClick={clearCart}>Clear Cart</button>
</div>
);
}
// src/components/CartIcon.js
function CartIcon() {
const { getItemCount } = useCart();
const itemCount = getItemCount();
return (
<div className="cart-icon">
🛒
{itemCount > 0 && (
<span className="cart-count">{itemCount}</span>
)}
</div>
);
}
// src/App.js
function App() {
return (
<CartProvider>
<div className="app">
<header>
<h1>E-commerce Store</h1>
<CartIcon />
</header>
<main>
<ProductList />
<Cart />
</main>
</div>
</CartProvider>
);
}
// Challenge: Add a wishlist feature
// Challenge: Add product categories and filtering
// Challenge: Add user authentication context
Test your understanding of this topic:
Master Redux Toolkit for predictable state management in large applications
Content by: Abhay Khanpara
MERN Stack Developer
Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. It includes utilities to simplify common Redux use cases.
// Install Redux Toolkit
npm install @reduxjs/toolkit react-redux
// Create a slice
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// Configure store
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Using Redux in components
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => dispatch(increment())}>
Increment
</button>
<button onClick={() => dispatch(decrement())}>
Decrement
</button>
</div>
);
}
// Async actions with createAsyncThunk
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
users: [],
status: 'idle',
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
}
});
Test your understanding of this topic:
Learn Zustand, a lightweight state management solution for React
Content by: Kriyansh Khunt
MERN Stack Developer
Zustand is a small, fast and scalable state management solution. It has a comfy API based on hooks and isn't boilerplate heavy or opinionated.
// Install Zustand
npm install zustand
// Create a store
import { create } from 'zustand';
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
// Using the store in components
function Counter() {
const { count, increment, decrement, reset } = useCounterStore();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
// Complex store with async actions
const useUserStore = create((set, get) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const users = await response.json();
set({ users, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
addUser: async (user) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
const newUser = await response.json();
set((state) => ({ users: [...state.users, newUser] }));
} catch (error) {
set({ error: error.message });
}
}
}));
// Store with persistence
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useAuthStore = create(
persist(
(set) => ({
user: null,
token: null,
login: (userData, token) => set({ user: userData, token }),
logout: () => set({ user: null, token: null })
}),
{
name: 'auth-storage', // unique name for localStorage key
getStorage: () => localStorage
}
)
);
// Using persisted store
function LoginForm() {
const { login } = useAuthStore();
const handleSubmit = async (e) => {
e.preventDefault();
// Simulate login
const userData = { id: 1, name: 'John Doe' };
const token = 'jwt-token';
login(userData, token);
};
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
Test your understanding of this topic:
Learn best practices and patterns for state management in React applications
Content by: Vijay Parmar
MERN Stack Developer
// 1. Keep state as local as possible
function UserProfile({ user }) {
const [isEditing, setIsEditing] = useState(false);
const [localUser, setLocalUser] = useState(user);
// Local state for UI interactions
const handleEdit = () => setIsEditing(true);
const handleCancel = () => {
setIsEditing(false);
setLocalUser(user); // Reset to original
};
return (
<div>
{isEditing ? (
<EditForm user={localUser} onSave={handleSave} onCancel={handleCancel} />
) : (
<UserDisplay user={user} onEdit={handleEdit} />
)}
</div>
);
}
// 2. Use custom hooks for state logic
function useUserManagement() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUsers = async () => {
setLoading(true);
try {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const addUser = async (user) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
const newUser = await response.json();
setUsers(prev => [...prev, newUser]);
} catch (err) {
setError(err.message);
}
};
return { users, loading, error, fetchUsers, addUser };
}
// 3. Combine multiple state management solutions
function App() {
return (
<Provider store={store}>
<ThemeProvider>
<UserProvider>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
</UserProvider>
</ThemeProvider>
</Provider>
);
}
Test your understanding of this topic:
Continue your learning journey and master the next set of concepts.
Continue to Module 5