State Management
Learning Objectives
- By the end of this lesson, you will be able to:
- - Use Context API for state management
- - Understand Redux basics
- - Apply state management patterns
- - Choose appropriate state management solution
- - Manage global state
- - Handle complex state logic
- - Build scalable applications
Lesson 26.3: State Management
Learning Objectives
By the end of this lesson, you will be able to:
- Use Context API for state management
- Understand Redux basics
- Apply state management patterns
- Choose appropriate state management solution
- Manage global state
- Handle complex state logic
- Build scalable applications
Introduction to State Management
State management is about how you store, organize, and update application state.
When to Use State Management?
- Local State: Component-specific (useState)
- Shared State: Multiple components (Context API, Redux)
- Global State: App-wide state (Context API, Redux)
- Complex State: Complex logic (Redux, Zustand)
State Management Solutions
// Built-in
useState // Local state
useContext // Shared state
useReducer // Complex local state
// Libraries
Redux // Predictable state container
Zustand // Lightweight state management
Jotai // Atomic state management
Recoil // Facebook's state management
Context API
What is Context API?
Context API is React's built-in solution for sharing state across components.
Basic Context
import { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
// Provider component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
Multiple Contexts
// Theme context
const ThemeContext = createContext();
// User context
const UserContext = createContext();
function App() {
return (
<ThemeProvider>
<UserProvider>
<Component />
</UserProvider>
</ThemeProvider>
);
}
Context with useReducer
import { createContext, useContext, useReducer } from 'react';
// Initial state
const initialState = {
count: 0,
todos: []
};
// Reducer
function appReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
default:
return state;
}
}
// Context
const AppContext = createContext();
// Provider
export function AppProvider({ children }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
// Hook
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
}
Example: Shopping Cart
// src/contexts/CartContext.jsx
import { 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
)
};
}
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;
}
};
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<CartContext.Provider
value={{
items: state.items,
total,
addItem,
removeItem,
updateQuantity,
clearCart
}}
>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
Redux Basics
What is Redux?
Redux is a predictable state container for JavaScript apps.
Redux Principles
- Single Source of Truth: One store
- State is Read-Only: Only actions change state
- Changes with Pure Functions: Reducers are pure
Installation
# Install Redux
npm install @reduxjs/toolkit react-redux
Basic Redux Setup
// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
// src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0
};
const counterSlice = createSlice({
name: 'counter',
initialState,
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;
// src/main.jsx
import { Provider } from 'react-redux';
import { store } from './store/store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
);
Using Redux
// src/components/Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount } from '../store/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(incrementByAmount(5))}>
+5
</button>
</div>
);
}
Redux with Async Actions
// src/store/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const response = await fetch('/api/todos');
return response.json();
}
);
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
loading: false,
error: null
},
reducers: {
addTodo: (state, action) => {
state.items.push(action.payload);
},
removeTodo: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
}
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
export const { addTodo, removeTodo } = todosSlice.actions;
export default todosSlice.reducer;
State Management Patterns
Pattern 1: Local State
// Use for component-specific state
function Component() {
const [count, setCount] = useState(0);
// ...
}
Pattern 2: Lifted State
// Lift state to common ancestor
function Parent() {
const [state, setState] = useState(initialState);
return (
<>
<Child1 state={state} setState={setState} />
<Child2 state={state} setState={setState} />
</>
);
}
Pattern 3: Context API
// Use for shared state across components
const Context = createContext();
function Provider({ children }) {
const [state, setState] = useState(initialState);
return (
<Context.Provider value={{ state, setState }}>
{children}
</Context.Provider>
);
}
Pattern 4: Redux
// Use for complex global state
const store = configureStore({
reducer: {
// Reducers
}
});
Pattern 5: Custom Hooks
// Encapsulate state logic
function useCustomState() {
const [state, setState] = useState(initialState);
// Logic
return { state, actions };
}
Practice Exercise
Exercise: State Management
Objective: Practice using Context API, Redux basics, and applying state management patterns.
Instructions:
- Create a React project
- Practice state management
- Practice:
- Context API
- Redux setup
- State management patterns
- Choosing the right solution
Example Solution:
// src/contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
setLoading(false);
}, []);
const login = (userData) => {
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
counter: counterReducer
}
});
// src/store/todosSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
filter: 'all'
};
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(item => item.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
removeTodo: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
setFilter: (state, action) => {
state.filter = action.payload;
}
}
});
export const { addTodo, toggleTodo, removeTodo, setFilter } = todosSlice.actions;
export default todosSlice.reducer;
// src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
reset: (state) => {
state.value = 0;
}
}
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;
// src/components/TodoList.jsx
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, removeTodo, setFilter } from '../store/todosSlice';
import { useState } from 'react';
function TodoList() {
const todos = useSelector((state) => state.todos.items);
const filter = useSelector((state) => state.todos.filter);
const dispatch = useDispatch();
const [input, setInput] = useState('');
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
const handleAdd = () => {
if (input.trim()) {
dispatch(addTodo(input));
setInput('');
}
};
return (
<div className="todo-list">
<h2>Todo List (Redux)</h2>
<div className="todo-input">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
placeholder="Add todo..."
/>
<button onClick={handleAdd}>Add</button>
</div>
<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => dispatch(setFilter('all'))}
>
All
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => dispatch(setFilter('active'))}
>
Active
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => dispatch(setFilter('completed'))}
>
Completed
</button>
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch(toggleTodo(todo.id))}
/>
<span>{todo.text}</span>
<button onClick={() => dispatch(removeTodo(todo.id))}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
export default TodoList;
// src/components/Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset } from '../store/counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div className="counter">
<h2>Counter (Redux)</h2>
<p>Count: {count}</p>
<div className="buttons">
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(reset())}>Reset</button>
<button onClick={() => dispatch(increment())}>+</button>
</div>
</div>
);
}
export default Counter;
// src/components/UserProfile.jsx
import { useAuth } from '../contexts/AuthContext';
function UserProfile() {
const { user, logout } = useAuth();
if (!user) {
return <p>Please log in</p>;
}
return (
<div className="user-profile">
<h2>User Profile (Context)</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
export default UserProfile;
// src/App.jsx
import { Provider } from 'react-redux';
import { store } from './store/store';
import { AuthProvider } from './contexts/AuthContext';
import Counter from './components/Counter';
import TodoList from './components/TodoList';
import UserProfile from './components/UserProfile';
import './App.css';
function App() {
return (
<Provider store={store}>
<AuthProvider>
<div className="App">
<header>
<h1>State Management Examples</h1>
</header>
<main>
<section>
<Counter />
</section>
<section>
<TodoList />
</section>
<section>
<UserProfile />
</section>
</main>
</div>
</AuthProvider>
</Provider>
);
}
export default App;
/* src/App.css */
.App {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
}
section {
margin-bottom: 40px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.counter, .todo-list {
max-width: 500px;
}
.buttons {
display: flex;
gap: 10px;
}
button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
button.active {
background-color: #28a745;
}
.todo-input {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-input input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
margin: 5px 0;
background-color: #f5f5f5;
border-radius: 4px;
}
li.completed span {
text-decoration: line-through;
opacity: 0.6;
}
Expected Output (in browser):
- Counter using Redux
- Todo list using Redux
- User profile using Context API
- All working together
Challenge (Optional):
- Build complex state management
- Combine Context and Redux
- Add async actions
- Build a complete app
Common Mistakes
1. Overusing Redux
// ❌ Bad: Redux for simple local state
const [count, setCount] = useState(0); // Should use useState
// ✅ Good: Redux for global/complex state
// Use Redux for shared state across many components
2. Not Splitting Contexts
// ❌ Bad: One context for everything
const AppContext = createContext();
// ✅ Good: Split contexts by concern
const ThemeContext = createContext();
const UserContext = createContext();
3. Mutating State in Redux
// ❌ Bad: Direct mutation (old Redux)
state.items.push(newItem);
// ✅ Good: Immutable updates (Redux Toolkit)
state.items.push(newItem); // OK with Redux Toolkit
// Or
state.items = [...state.items, newItem];
Key Takeaways
- Context API: Built-in solution for shared state
- Redux: Predictable state container
- Patterns: Choose right solution for use case
- Local State: useState for component-specific
- Shared State: Context API for shared
- Global State: Redux for complex global state
- Best Practice: Don't overuse, split by concern
Quiz: State Management
Test your understanding with these questions:
-
Context API:
- A) Built-in React
- B) External library
- C) Both
- D) Neither
-
Redux:
- A) Predictable state
- B) Unpredictable state
- C) Both
- D) Neither
-
useState:
- A) Local state
- B) Global state
- C) Both
- D) Neither
-
Context API:
- A) Shared state
- B) Local state
- C) Both
- D) Neither
-
Redux:
- A) Complex state
- B) Simple state
- C) Both
- D) Neither
-
State management:
- A) Choose by use case
- B) Always use Redux
- C) Both
- D) Neither
-
Redux Toolkit:
- A) Modern Redux
- B) Old Redux
- C) Both
- D) Neither
Answers:
- A) Built-in React
- A) Predictable state
- A) Local state
- A) Shared state
- A) Complex state
- A) Choose by use case
- A) Modern Redux
Next Steps
Congratulations! You've completed Module 26: React Advanced. You now know:
- React hooks (useEffect, useContext, useReducer)
- React Router
- State management (Context API, Redux)
- How to build advanced React applications
What's Next?
- Module 27: Vue.js (Alternative Framework)
- Learn Vue.js basics
- Compare with React
- Build Vue applications
Additional Resources
- Context API: react.dev/reference/react/useContext
- Redux: redux.js.org
- Redux Toolkit: redux-toolkit.js.org
- State Management Guide: react.dev/learn/managing-state
Lesson completed! You've finished Module 26: React Advanced. Ready for Module 27: Vue.js!
Course Navigation
React Basics
React Advanced
- React Hooks
- React Router
- State Management
React Advanced
- React Hooks
- React Router
- State Management