State management with Context API and Redux patterns
Alex Chang
Feb 2026
2 tabs
import React, { createContext, useContext, useReducer, useState } from 'react';
// 1. Basic Context
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => setUser(userData);
const logout = () => setUser(null);
const value = {
user,
login,
logout,
isAuthenticated: !!user,
};
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// Custom hook for using context
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// Usage
function LoginButton() {
const { login, logout, isAuthenticated, user } = useUser();
return isAuthenticated ? (
<div>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</div>
) : (
<button onClick={() => login({ id: 1, name: 'Alex' })}>
Login
</button>
);
}
// 2. Context with useReducer
const CartContext = createContext(null);
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
const existing = state.items.find(item => item.id === action.payload.id);
if (existing) {
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;
}
};
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
);
const value = {
items: state.items,
addItem,
removeItem,
updateQuantity,
clearCart,
total,
itemCount: state.items.reduce((sum, item) => sum + item.quantity, 0),
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
// 3. Multiple contexts composed
function App() {
return (
<UserProvider>
<CartProvider>
<ThemeProvider>
<MainApp />
</ThemeProvider>
</CartProvider>
</UserProvider>
);
}
// 4. Context with TypeScript-style interface
/*
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
*/
const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Redux Toolkit approach (modern)
import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit';
// 1. Create slice (actions + reducer together)
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Immer allows "mutations"
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
// Export actions
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 2. Async actions with createAsyncThunk
export const fetchUser = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch');
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
loading: 'idle',
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = 'pending';
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.entities[action.payload.id] = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = 'failed';
state.error = action.payload;
});
},
});
// 3. Configure store
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
users: usersSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
// 4. React component using Redux
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}
function UserProfile({ userId }) {
const dispatch = useDispatch();
const user = useSelector((state) => state.users.entities[userId]);
const loading = useSelector((state) => state.users.loading);
const error = useSelector((state) => state.users.error);
useEffect(() => {
if (!user) {
dispatch(fetchUser(userId));
}
}, [userId, user, dispatch]);
if (loading === 'pending') return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}
// 5. Selectors (memoized with reselect)
import { createSelector } from '@reduxjs/toolkit';
const selectAllTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectAllTodos, selectFilter],
(todos, filter) => {
if (filter === 'active') return todos.filter(t => !t.completed);
if (filter === 'completed') return todos.filter(t => t.completed);
return todos;
}
);
// Usage
function TodoList() {
const todos = useSelector(selectFilteredTodos);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
// 6. Classic Redux pattern (for comparison)
// Actions
const INCREMENT = 'counter/increment';
const DECREMENT = 'counter/decrement';
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
// Reducer
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case INCREMENT:
return { ...state, value: state.value + 1 };
case DECREMENT:
return { ...state, value: state.value - 1 };
default:
return state;
}
}
2 files · javascript
Explain with highlit
State management solutions handle data flow in complex applications. I use React Context API for moderate state sharing without prop drilling. The createContext function creates context objects, while Provider passes data down the tree. The useReducer hook manages complex state logic with actions and reducers. Redux provides centralized state management with unidirectional data flow. Actions describe what happened, reducers specify state updates, and store holds application state. The useSelector hook reads state, while useDispatch dispatches actions. Modern Redux Toolkit simplifies boilerplate with createSlice. Proper state management improves maintainability.