Skip to main content

Chapter 6: Context API & State Management - Building Scalable React Applications

Welcome to the comprehensive guide to React's Context API and advanced state management! In this chapter, we'll explore how to build scalable, maintainable applications with proper state management patterns.

Learning Objectives

By the end of this chapter, you will understand:

  • What the Context API is and why it's essential for modern React applications
  • How to create and consume contexts effectively and efficiently
  • Why different state management patterns exist and when to use each one
  • What external state management libraries offer and how to choose between them
  • How to optimize context performance and avoid common pitfalls
  • What best practices are for building maintainable state management systems

What is the Context API? The Foundation of Global State Management

What is the Context API?

The Context API is React's built-in solution for sharing data across the component tree without having to pass props down manually at every level. It's designed to solve the "props drilling" problem and enable global state management.

The Context API is what enables you to share state across multiple components without the complexity of external state management libraries.

What Problems Does the Context API Solve?

The Context API addresses several common challenges in React applications:

  1. Props Drilling: Eliminates the need to pass props through multiple component layers
  2. Global State: Provides a way to share state across the entire component tree
  3. Theme Management: Enables consistent theming across components
  4. User Authentication: Shares user data and authentication state
  5. Language Settings: Manages internationalization and localization
  6. Configuration: Shares app-wide configuration and settings

What Makes Context API Powerful?

The Context API provides several key benefits:

  1. Built-in Solution: No external dependencies required
  2. Type Safety: Works well with TypeScript
  3. Performance: Optimized for React's rendering system
  4. Flexibility: Can be used for simple or complex state management
  5. Developer Experience: Integrated with React DevTools

How to Implement Context API? The Technical Implementation

How to Create and Use Basic Context?

import React, { createContext, useContext, useState } from 'react';

// Create context
const ThemeContext = createContext();

// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');

const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};

const value = {
theme,
toggleTheme
};

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

// Custom hook for consuming context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}

// Components using the context
function Header() {
const { theme, toggleTheme } = useTheme();

return (
<header style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
padding: '1rem'
}}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
</header>
);
}

function Content() {
const { theme } = useTheme();

return (
<main style={{
backgroundColor: theme === 'light' ? '#f5f5f5' : '#222',
color: theme === 'light' ? '#333' : '#fff',
padding: '2rem',
minHeight: '400px'
}}>
<h2>Welcome to our app!</h2>
<p>This content adapts to the current theme.</p>
</main>
);
}

function App() {
return (
<ThemeProvider>
<Header />
<Content />
</ThemeProvider>
);
}

How to Implement Complex Context with useReducer?

import React, { createContext, useContext, useReducer } from 'react';

// Initial state
const initialState = {
user: null,
theme: 'light',
notifications: [],
isLoading: false,
error: null
};

// Action types
const ActionTypes = {
SET_LOADING: 'SET_LOADING',
SET_USER: 'SET_USER',
SET_THEME: 'SET_THEME',
ADD_NOTIFICATION: 'ADD_NOTIFICATION',
REMOVE_NOTIFICATION: 'REMOVE_NOTIFICATION',
SET_ERROR: 'SET_ERROR',
CLEAR_ERROR: 'CLEAR_ERROR'
};

// Reducer
function appReducer(state, action) {
switch (action.type) {
case ActionTypes.SET_LOADING:
return { ...state, isLoading: action.payload };

case ActionTypes.SET_USER:
return { ...state, user: action.payload, error: null };

case ActionTypes.SET_THEME:
return { ...state, theme: action.payload };

case ActionTypes.ADD_NOTIFICATION:
return {
...state,
notifications: [...state.notifications, {
id: Date.now(),
...action.payload
}]
};

case ActionTypes.REMOVE_NOTIFICATION:
return {
...state,
notifications: state.notifications.filter(n => n.id !== action.payload)
};

case ActionTypes.SET_ERROR:
return { ...state, error: action.payload, isLoading: false };

case ActionTypes.CLEAR_ERROR:
return { ...state, error: null };

default:
return state;
}
}

// Context
const AppContext = createContext();

// Provider
function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);

// Action creators
const actions = {
setLoading: (loading) => dispatch({ type: ActionTypes.SET_LOADING, payload: loading }),

setUser: (user) => dispatch({ type: ActionTypes.SET_USER, payload: user }),

setTheme: (theme) => dispatch({ type: ActionTypes.SET_THEME, payload: theme }),

addNotification: (notification) => dispatch({
type: ActionTypes.ADD_NOTIFICATION,
payload: notification
}),

removeNotification: (id) => dispatch({
type: ActionTypes.REMOVE_NOTIFICATION,
payload: id
}),

setError: (error) => dispatch({ type: ActionTypes.SET_ERROR, payload: error }),

clearError: () => dispatch({ type: ActionTypes.CLEAR_ERROR })
};

return (
<AppContext.Provider value={{ state, actions }}>
{children}
</AppContext.Provider>
);
}

// Custom hook
function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
}

export { AppProvider, useApp };

Multiple Contexts Pattern

// User Context
const UserContext = createContext();

function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);

const login = async (credentials) => {
setIsLoading(true);
try {
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 {
setIsLoading(false);
}
};

const logout = () => {
setUser(null);
};

return (
<UserContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</UserContext.Provider>
);
}

// Theme Context
const ThemeContext = createContext();

function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');

const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};

const colors = {
light: { bg: '#fff', text: '#000', primary: '#007bff' },
dark: { bg: '#333', text: '#fff', primary: '#0d6efd' }
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme, colors: colors[theme] }}>
{children}
</ThemeContext.Provider>
);
}

// Notification Context
const NotificationContext = createContext();

function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);

const addNotification = (notification) => {
const id = Date.now();
setNotifications(prev => [...prev, { ...notification, id }]);

// Auto-remove after 5 seconds
setTimeout(() => {
removeNotification(id);
}, 5000);
};

const removeNotification = (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};

return (
<NotificationContext.Provider value={{
notifications,
addNotification,
removeNotification
}}>
{children}
</NotificationContext.Provider>
);
}

// App with multiple contexts
function App() {
return (
<UserProvider>
<ThemeProvider>
<NotificationProvider>
<Header />
<Main />
<NotificationList />
</NotificationProvider>
</ThemeProvider>
</UserProvider>
);
}

Redux Fundamentals

Basic Redux Setup

import { createStore } from 'redux';

// Action types
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';
const SET_FILTER = 'SET_FILTER';

// Action creators
const addTodo = (text) => ({
type: ADD_TODO,
payload: { id: Date.now(), text, completed: false }
});

const toggleTodo = (id) => ({
type: TOGGLE_TODO,
payload: id
});

const deleteTodo = (id) => ({
type: DELETE_TODO,
payload: id
});

const setFilter = (filter) => ({
type: SET_FILTER,
payload: filter
});

// Reducer
function todoReducer(state = { todos: [], filter: 'all' }, action) {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
case TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case SET_FILTER:
return {
...state,
filter: action.payload
};
default:
return state;
}
}

// Create store
const store = createStore(todoReducer);

// React-Redux usage
import { Provider, useSelector, useDispatch } from 'react-redux';

function TodoApp() {
return (
<Provider store={store}>
<TodoContainer />
</Provider>
);
}

function TodoContainer() {
const todos = useSelector(state => state.todos);
const filter = useSelector(state => state.filter);
const dispatch = useDispatch();

const filteredTodos = todos.filter(todo => {
switch (filter) {
case 'active': return !todo.completed;
case 'completed': return todo.completed;
default: return true;
}
});

return (
<div>
<TodoInput onAdd={(text) => dispatch(addTodo(text))} />
<TodoFilter
filter={filter}
onFilterChange={(filter) => dispatch(setFilter(filter))}
/>
<TodoList
todos={filteredTodos}
onToggle={(id) => dispatch(toggleTodo(id))}
onDelete={(id) => dispatch(deleteTodo(id))}
/>
</div>
);
}

Redux Toolkit (RTK)

import { createSlice, configureStore } from '@reduxjs/toolkit';

// Todo slice
const todoSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all',
loading: false,
error: null
},
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false,
createdAt: new Date().toISOString()
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(todo => todo.id !== action.payload);
},
setFilter: (state, action) => {
state.filter = action.payload;
},
setLoading: (state, action) => {
state.loading = action.payload;
},
setError: (state, action) => {
state.error = action.payload;
}
}
});

// User slice
const userSlice = createSlice({
name: 'user',
initialState: {
currentUser: null,
isAuthenticated: false,
loading: false
},
reducers: {
setUser: (state, action) => {
state.currentUser = action.payload;
state.isAuthenticated = !!action.payload;
},
logout: (state) => {
state.currentUser = null;
state.isAuthenticated = false;
},
setLoading: (state, action) => {
state.loading = action.payload;
}
}
});

// Configure store
const store = configureStore({
reducer: {
todos: todoSlice.reducer,
user: userSlice.reducer
}
});

// Export actions
export const { addTodo, toggleTodo, deleteTodo, setFilter } = todoSlice.actions;
export const { setUser, logout } = userSlice.actions;

// Usage in components
function TodoApp() {
return (
<Provider store={store}>
<TodoContainer />
</Provider>
);
}

function TodoContainer() {
const todos = useSelector(state => state.todos.items);
const filter = useSelector(state => state.todos.filter);
const dispatch = useDispatch();

const handleAddTodo = (text) => {
dispatch(addTodo(text));
};

const handleToggleTodo = (id) => {
dispatch(toggleTodo(id));
};

const handleDeleteTodo = (id) => {
dispatch(deleteTodo(id));
};

const handleSetFilter = (filter) => {
dispatch(setFilter(filter));
};

return (
<div>
<TodoInput onAdd={handleAddTodo} />
<TodoFilter filter={filter} onFilterChange={handleSetFilter} />
<TodoList
todos={todos}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
/>
</div>
);
}

Zustand State Management

Zustand is a lightweight alternative to Redux with a simpler API.

Basic Zustand Store

import { create } from 'zustand';

// Basic store
const useTodoStore = create((set) => ({
todos: [],
filter: 'all',

addTodo: (text) => set((state) => ({
todos: [...state.todos, {
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
}]
})),

toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),

deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),

setFilter: (filter) => set({ filter }),

clearCompleted: () => set((state) => ({
todos: state.todos.filter(todo => !todo.completed)
}))
}));

// Usage in components
function TodoApp() {
const { todos, filter, addTodo, toggleTodo, deleteTodo, setFilter } = useTodoStore();

const filteredTodos = todos.filter(todo => {
switch (filter) {
case 'active': return !todo.completed;
case 'completed': return todo.completed;
default: return true;
}
});

return (
<div>
<TodoInput onAdd={addTodo} />
<TodoFilter filter={filter} onFilterChange={setFilter} />
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}

Advanced Zustand with Middleware

import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';

// Store with persistence and devtools
const useAppStore = create(
devtools(
persist(
(set, get) => ({
// User state
user: null,
isAuthenticated: false,

// Todo state
todos: [],
filter: 'all',

// Theme state
theme: 'light',

// Actions
setUser: (user) => set({ user, isAuthenticated: !!user }),
logout: () => set({ user: null, isAuthenticated: false }),

addTodo: (text) => set((state) => ({
todos: [...state.todos, {
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
}]
})),

toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),

deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
})),

setFilter: (filter) => set({ filter }),

setTheme: (theme) => set({ theme }),

// Computed values
getFilteredTodos: () => {
const { todos, filter } = get();
switch (filter) {
case 'active': return todos.filter(todo => !todo.completed);
case 'completed': return todos.filter(todo => todo.completed);
default: return todos;
}
},

getStats: () => {
const { todos } = get();
return {
total: todos.length,
completed: todos.filter(todo => todo.completed).length,
active: todos.filter(todo => !todo.completed).length
};
}
}),
{
name: 'app-storage', // localStorage key
partialize: (state) => ({
todos: state.todos,
theme: state.theme
}) // Only persist these fields
}
),
{ name: 'app-store' } // DevTools name
)
);

// Usage
function App() {
const {
user,
isAuthenticated,
setUser,
logout,
theme,
setTheme,
getFilteredTodos,
getStats
} = useAppStore();

const todos = getFilteredTodos();
const stats = getStats();

return (
<div className={`app ${theme}`}>
<Header
user={user}
isAuthenticated={isAuthenticated}
onLogin={setUser}
onLogout={logout}
theme={theme}
onThemeChange={setTheme}
/>
<Main todos={todos} stats={stats} />
</div>
);
}

State Management Patterns

Selector Pattern

// Redux selectors
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active': return todos.filter(todo => !todo.completed);
case 'completed': return todos.filter(todo => todo.completed);
default: return todos;
}
}
);

// Usage
function TodoList() {
const todos = useSelector(selectFilteredTodos);
// Component logic...
}

// Zustand selectors
const useFilteredTodos = () => useTodoStore((state) => {
const { todos, filter } = state;
switch (filter) {
case 'active': return todos.filter(todo => !todo.completed);
case 'completed': return todos.filter(todo => todo.completed);
default: return todos;
}
});

// Usage
function TodoList() {
const todos = useFilteredTodos();
// Component logic...
}

Normalized State Pattern

// Normalized state structure
const initialState = {
entities: {
users: {
byId: {
1: { id: 1, name: 'John', email: '[email protected]' },
2: { id: 2, name: 'Jane', email: '[email protected]' }
},
allIds: [1, 2]
},
posts: {
byId: {
1: { id: 1, title: 'Post 1', authorId: 1, content: '...' },
2: { id: 2, title: 'Post 2', authorId: 2, content: '...' }
},
allIds: [1, 2]
}
},
ui: {
selectedUserId: null,
loading: false,
error: null
}
};

// Selectors for normalized state
const selectUserById = (state, userId) =>
state.entities.users.byId[userId];

const selectPostsByUserId = (state, userId) =>
state.entities.posts.allIds
.map(id => state.entities.posts.byId[id])
.filter(post => post.authorId === userId);

// Reducer for normalized state
function entitiesReducer(state = initialState.entities, action) {
switch (action.type) {
case 'ADD_USER':
return {
...state,
users: {
byId: {
...state.users.byId,
[action.payload.id]: action.payload
},
allIds: [...state.users.allIds, action.payload.id]
}
};
case 'ADD_POST':
return {
...state,
posts: {
byId: {
...state.posts.byId,
[action.payload.id]: action.payload
},
allIds: [...state.posts.allIds, action.payload.id]
}
};
default:
return state;
}
}

Performance Optimization

Context Optimization

// Split contexts to prevent unnecessary re-renders
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

// Memoized context values
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);

// Memoize context values
const userValue = useMemo(() => ({
user,
setUser
}), [user]);

const themeValue = useMemo(() => ({
theme,
setTheme
}), [theme]);

const notificationValue = useMemo(() => ({
notifications,
addNotification: (notification) => {
setNotifications(prev => [...prev, notification]);
},
removeNotification: (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
}
}), [notifications]);

return (
<UserContext.Provider value={userValue}>
<ThemeContext.Provider value={themeValue}>
<NotificationContext.Provider value={notificationValue}>
{children}
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
);
}

Redux Performance

// Memoized selectors
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;

const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
console.log('Computing filtered todos...');
switch (filter) {
case 'active': return todos.filter(todo => !todo.completed);
case 'completed': return todos.filter(todo => todo.completed);
default: return todos;
}
}
);

// Memoized component
const TodoList = React.memo(function TodoList({ todos, onToggle, onDelete }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
});

// Usage with memoized selectors
function TodoContainer() {
const todos = useSelector(selectFilteredTodos);
const dispatch = useDispatch();

const handleToggle = useCallback((id) => {
dispatch(toggleTodo(id));
}, [dispatch]);

const handleDelete = useCallback((id) => {
dispatch(deleteTodo(id));
}, [dispatch]);

return (
<TodoList
todos={todos}
onToggle={handleToggle}
onDelete={handleDelete}
/>
);
}

Practice Project: E-commerce Store

Let's build a complete e-commerce application using different state management approaches:

import React, { createContext, useContext, useReducer, useState } from 'react';

// Product Context with useReducer
const ProductContext = createContext();

const productReducer = (state, action) => {
switch (action.type) {
case 'SET_PRODUCTS':
return { ...state, products: action.payload, loading: false };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'SET_CATEGORY_FILTER':
return { ...state, categoryFilter: action.payload };
case 'SET_PRICE_FILTER':
return { ...state, priceFilter: action.payload };
case 'SET_SORT_BY':
return { ...state, sortBy: action.payload };
default:
return state;
}
};

function ProductProvider({ children }) {
const [state, dispatch] = useReducer(productReducer, {
products: [],
loading: false,
error: null,
categoryFilter: 'all',
priceFilter: { min: 0, max: 1000 },
sortBy: 'name'
});

const actions = {
setProducts: (products) => dispatch({ type: 'SET_PRODUCTS', payload: products }),
setLoading: (loading) => dispatch({ type: 'SET_LOADING', payload: loading }),
setError: (error) => dispatch({ type: 'SET_ERROR', payload: error }),
setCategoryFilter: (category) => dispatch({ type: 'SET_CATEGORY_FILTER', payload: category }),
setPriceFilter: (priceFilter) => dispatch({ type: 'SET_PRICE_FILTER', payload: priceFilter }),
setSortBy: (sortBy) => dispatch({ type: 'SET_SORT_BY', payload: sortBy })
};

return (
<ProductContext.Provider value={{ state, actions }}>
{children}
</ProductContext.Provider>
);
}

// Cart Context
const CartContext = createContext();

function CartProvider({ children }) {
const [cart, setCart] = useState([]);

const addToCart = (product) => {
setCart(prev => {
const existingItem = prev.find(item => item.id === product.id);
if (existingItem) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};

const removeFromCart = (productId) => {
setCart(prev => prev.filter(item => item.id !== productId));
};

const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeFromCart(productId);
return;
}
setCart(prev => prev.map(item =>
item.id === productId ? { ...item, quantity } : item
));
};

const clearCart = () => setCart([]);

const getCartTotal = () => {
return cart.reduce((total, item) => total + (item.price * item.quantity), 0);
};

const getCartItemCount = () => {
return cart.reduce((count, item) => count + item.quantity, 0);
};

return (
<CartContext.Provider value={{
cart,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
getCartTotal,
getCartItemCount
}}>
{children}
</CartContext.Provider>
);
}

// Main App
function EcommerceApp() {
return (
<ProductProvider>
<CartProvider>
<Header />
<ProductGrid />
<CartSidebar />
</CartProvider>
</ProductProvider>
);
}

// Header Component
function Header() {
const { getCartItemCount } = useContext(CartContext);
const [isCartOpen, setIsCartOpen] = useState(false);

return (
<header className="header">
<h1>E-commerce Store</h1>
<button
className="cart-button"
onClick={() => setIsCartOpen(!isCartOpen)}
>
Cart ({getCartItemCount()})
</button>
</header>
);
}

// Product Grid
function ProductGrid() {
const { state, actions } = useContext(ProductContext);
const { addToCart } = useContext(CartContext);

// Filter and sort products
const filteredProducts = state.products
.filter(product => {
const categoryMatch = state.categoryFilter === 'all' ||
product.category === state.categoryFilter;
const priceMatch = product.price >= state.priceFilter.min &&
product.price <= state.priceFilter.max;
return categoryMatch && priceMatch;
})
.sort((a, b) => {
switch (state.sortBy) {
case 'price-low':
return a.price - b.price;
case 'price-high':
return b.price - a.price;
case 'name':
default:
return a.name.localeCompare(b.name);
}
});

return (
<div className="product-grid">
<ProductFilters
categoryFilter={state.categoryFilter}
priceFilter={state.priceFilter}
sortBy={state.sortBy}
onCategoryChange={actions.setCategoryFilter}
onPriceChange={actions.setPriceFilter}
onSortChange={actions.setSortBy}
/>

<div className="products">
{filteredProducts.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={addToCart}
/>
))}
</div>
</div>
);
}

// Product Card
function ProductCard({ product, onAddToCart }) {
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">${product.price}</p>
<p className="category">{product.category}</p>
<button onClick={() => onAddToCart(product)}>
Add to Cart
</button>
</div>
);
}

// Cart Sidebar
function CartSidebar() {
const {
cart,
removeFromCart,
updateQuantity,
getCartTotal,
clearCart
} = useContext(CartContext);

return (
<div className="cart-sidebar">
<h2>Shopping Cart</h2>

{cart.length === 0 ? (
<p>Your cart is empty</p>
) : (
<>
<div className="cart-items">
{cart.map(item => (
<div key={item.id} className="cart-item">
<img src={item.image} alt={item.name} />
<div className="item-details">
<h4>{item.name}</h4>
<p>${item.price}</p>
<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>
))}
</div>

<div className="cart-summary">
<h3>Total: ${getCartTotal().toFixed(2)}</h3>
<button onClick={clearCart}>Clear Cart</button>
<button className="checkout-button">Checkout</button>
</div>
</>
)}
</div>
);
}

export default EcommerceApp;

Summary

In this chapter, we explored advanced state management in React:

  • Advanced Context API: useReducer with Context, multiple contexts
  • Redux: Basic Redux and Redux Toolkit
  • Zustand: Lightweight state management
  • State Patterns: Selectors, normalized state
  • Performance: Optimization techniques
  • Best Practices: When to use different solutions

Next Steps

Additional Resources