Learn when to use Context API vs Redux and how to make informed architectural decisions.
Learn when to use Context API vs Redux and how to make informed architectural decisions.
Choosing between Context API and Redux is one of the most important architectural decisions in React applications. Both are powerful tools for state management, but they excel in different scenarios.
The React community has long debated whether to use Context API or Redux for state management. The truth is: both have their place, and the best choice depends on your specific requirements.
1. Simple State Sharing
1// Perfect for theme, user authentication, language settings
2const ThemeContext = createContext();
3
4function ThemeProvider({ children }) {
5 const [theme, setTheme] = useState('light');
6
7 return (
8 <ThemeContext.Provider value={{ theme, setTheme }}>
9 {children}
10 </ThemeContext.Provider>
11 );
12}2. Built-in to React
3. Component-Centric
4. Minimal Setup
1. Performance Issues
1// ❌ This causes ALL consumers to re-render
2const AppContext = createContext();
3
4function AppProvider({ children }) {
5 const [user, setUser] = useState(null);
6 const [todos, setTodos] = useState([]);
7 const [theme, setTheme] = useState('light');
8
9 // Any change re-renders all consumers!
10 const value = { user, setUser, todos, setTodos, theme, setTheme };
11
12 return (
13 <AppContext.Provider value={value}>
14 {children}
15 </AppContext.Provider>
16 );
17}2. No Built-in Middleware
3. Scaling Challenges
4. Testing Complexity
1. Predictable State Management
1// Clear, testable, predictable updates
2const todosSlice = createSlice({
3 name: 'todos',
4 initialState: { items: [], loading: false },
5 reducers: {
6 addTodo: (state, action) => {
7 state.items.push(action.payload);
8 },
9 toggleTodo: (state, action) => {
10 const todo = state.items.find(t => t.id === action.payload);
11 if (todo) todo.completed = !todo.completed;
12 }
13 }
14});2. Powerful Ecosystem
3. Performance Optimization
1// Only re-renders when specific data changes
2const todos = useSelector(state => state.todos.items);
3const loading = useSelector(state => state.todos.loading);4. Scalability
5. Testing Excellence
1// Easy to test reducers in isolation
2describe('todosSlice', () => {
3 it('should add todo', () => {
4 const state = { items: [] };
5 const action = addTodo({ id: 1, text: 'Test' });
6 const newState = todosSlice.reducer(state, action);
7 expect(newState.items).toHaveLength(1);
8 });
9});1. Learning Curve
2. Boilerplate (Reduced with RTK)
3. Over-engineering Risk
1// ❌ Performance Problem
2const AppContext = createContext();
3
4function App() {
5 const [count, setCount] = useState(0);
6 const [user, setUser] = useState(null);
7
8 // Creates new object on every render!
9 const value = { count, setCount, user, setUser };
10
11 return (
12 <AppContext.Provider value={value}>
13 <ExpensiveComponent /> {/* Re-renders on ANY change */}
14 <AnotherComponent /> {/* Re-renders on ANY change */}
15 </AppContext.Provider>
16 );
17}
18
19// ✅ Performance Solution
20function App() {
21 const [count, setCount] = useState(0);
22 const [user, setUser] = useState(null);
23
24 // Memoize the value
25 const value = useMemo(
26 () => ({ count, setCount, user, setUser }),
27 [count, user]
28 );
29
30 return (
31 <AppContext.Provider value={value}>
32 <Components />
33 </AppContext.Provider>
34 );
35}1// ✅ Automatic Performance Optimization
2function TodoList() {
3 // Only re-renders when todos change
4 const todos = useSelector(state => state.todos);
5
6 return (
7 <ul>
8 {todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
9 </ul>
10 );
11}
12
13function UserProfile() {
14 // Only re-renders when user changes
15 const user = useSelector(state => state.user);
16
17 return <div>{user.name}</div>;
18}1. Simple Global State
2. Component Library Authors
3. Small to Medium Applications
4. Tight Component Coupling
1// ✅ Perfect for Context
2const UserContext = createContext();
3const ThemeContext = createContext();
4const LocalizationContext = createContext();
5
6// Simple, rarely changing, consumed widely
7function App() {
8 return (
9 <UserProvider>
10 <ThemeProvider>
11 <LocalizationProvider>
12 <Router />
13 </LocalizationProvider>
14 </ThemeProvider>
15 </UserProvider>
16 );
17}1. Complex State Management
2. Large Applications
3. Advanced Requirements
4. Performance Critical
1// ✅ Perfect for Redux
2const store = configureStore({
3 reducer: {
4 users: usersSlice.reducer,
5 posts: postsSlice.reducer,
6 comments: commentsSlice.reducer,
7 notifications: notificationsSlice.reducer,
8 ui: uiSlice.reducer,
9 cache: cacheSlice.reducer,
10 realtime: realtimeSlice.reducer
11 }
12});
13
14// Complex, interconnected, frequently changing| Factor | Context API | Redux |
|--------|-------------|-------|
| Learning Curve | ✅ Low | ❌ Medium-High |
| Setup Time | ✅ Minimal | ❌ Moderate |
| Performance | ❌ Manual optimization | ✅ Built-in optimization |
| Debugging | ❌ Limited tools | ✅ Excellent tools |
| Testing | ❌ Complex | ✅ Straightforward |
| Scalability | ❌ Challenging | ✅ Excellent |
| Team Onboarding | ✅ Easy | ❌ Requires training |
| Bundle Size | ✅ Zero overhead | ❌ Additional KB |
| Async Handling | ❌ Manual | ✅ Built-in patterns |
| State Persistence | ❌ Manual | ✅ Ecosystem solutions |
1// ✅ Hybrid Strategy
2function App() {
3 return (
4 <Provider store={reduxStore}>
5 <UserProvider> {/* Context for user auth */}
6 <ThemeProvider> {/* Context for theme */}
7 <Router /> {/* Redux for app data */}
8 </ThemeProvider>
9 </UserProvider>
10 </Provider>
11 );
12}
13
14// Redux for complex data
15const appDataSlice = createSlice({
16 name: 'appData',
17 initialState: { todos: [], posts: [], comments: [] },
18 reducers: { /* complex logic */ }
19});
20
21// Context for simple global state
22const UserContext = createContext();
23const ThemeContext = createContext();Phase 1: Preparation
1// Start by isolating context logic
2const useUserContext = () => {
3 const context = useContext(UserContext);
4 return context;
5};
6
7// Create adapter hooks
8const useUser = () => {
9 return useUserContext(); // Later: useSelector(state => state.user)
10};Phase 2: Gradual Migration
1// Migrate piece by piece
2const store = configureStore({
3 reducer: {
4 user: userSlice.reducer,
5 // Keep existing context for other data
6 }
7});
8
9// Update hooks gradually
10const useUser = () => {
11 return useSelector(state => state.user); // Migrated
12};
13
14const useTheme = () => {
15 return useContext(ThemeContext); // Still context
16};When to Consider:
Approach:
1// Extract simple state to context
2const SimpleStateContext = createContext();
3
4// Keep complex state in Redux
5const complexStore = configureStore({
6 reducer: {
7 // Only complex reducers remain
8 dataProcessing: dataSlice.reducer,
9 realtime: realtimeSlice.reducer
10 }
11});1// ❌ Single context causes unnecessary re-renders
2const AppContext = createContext();
3
4function AppProvider({ children }) {
5 const [user, setUser] = useState(null); // Changes rarely
6 const [notifications, setNotifications] = useState([]); // Changes frequently
7
8 const value = { user, setUser, notifications, setNotifications };
9
10 return (
11 <AppContext.Provider value={value}>
12 {children}
13 </AppContext.Provider>
14 );
15}
16
17// ✅ Split contexts by update frequency
18const UserContext = createContext(); // Rarely changes
19const NotificationsContext = createContext(); // Frequently changes
20
21function UserProvider({ children }) {
22 const [user, setUser] = useState(null);
23 const value = useMemo(() => ({ user, setUser }), [user]);
24
25 return (
26 <UserContext.Provider value={value}>
27 {children}
28 </UserContext.Provider>
29 );
30}
31
32function NotificationsProvider({ children }) {
33 const [notifications, setNotifications] = useState([]);
34 const value = useMemo(
35 () => ({ notifications, setNotifications }),
36 [notifications]
37 );
38
39 return (
40 <NotificationsContext.Provider value={value}>
41 {children}
42 </NotificationsContext.Provider>
43 );
44}1// ✅ Properly memoized context
2function AppProvider({ children }) {
3 const [state, setState] = useState(initialState);
4
5 // Memoize the value object
6 const value = useMemo(() => ({
7 state,
8 actions: {
9 updateUser: (user) => setState(prev => ({ ...prev, user })),
10 updateTheme: (theme) => setState(prev => ({ ...prev, theme })),
11 }
12 }), [state]);
13
14 return (
15 <AppContext.Provider value={value}>
16 {children}
17 </AppContext.Provider>
18 );
19}1// ✅ Separate state and dispatch contexts
2const StateContext = createContext();
3const DispatchContext = createContext();
4
5function AppProvider({ children }) {
6 const [state, dispatch] = useReducer(appReducer, initialState);
7
8 return (
9 <StateContext.Provider value={state}>
10 <DispatchContext.Provider value={dispatch}>
11 {children}
12 </DispatchContext.Provider>
13 </StateContext.Provider>
14 );
15}
16
17// Components only subscribe to what they need
18function UserProfile() {
19 const { user } = useContext(StateContext); // Only re-renders when state changes
20 const dispatch = useContext(DispatchContext); // Never re-renders
21
22 return <div onClick={() => dispatch(updateUser())}>{user.name}</div>;
23}1// ✅ Custom hooks for selective consumption
2function useUser() {
3 const { user } = useContext(AppContext);
4 return user;
5}
6
7function useTheme() {
8 const { theme } = useContext(AppContext);
9 return theme;
10}
11
12// Component only re-renders when user changes
13function UserComponent() {
14 const user = useUser(); // Doesn't care about theme changes
15 return <div>{user.name}</div>;
16}1// ❌ Creates new array on every render
2const TodoList = () => {
3 const completedTodos = useSelector(state =>
4 state.todos.filter(todo => todo.completed) // New array every time!
5 );
6
7 return <ul>{completedTodos.map(renderTodo)}</ul>;
8};
9
10// ✅ Memoized selector
11import { createSelector } from '@reduxjs/toolkit';
12
13const selectCompletedTodos = createSelector(
14 [state => state.todos],
15 (todos) => todos.filter(todo => todo.completed)
16);
17
18const TodoList = () => {
19 const completedTodos = useSelector(selectCompletedTodos);
20 return <ul>{completedTodos.map(renderTodo)}</ul>;
21};1// ❌ Nested structure causes unnecessary re-renders
2const initialState = {
3 posts: [
4 {
5 id: 1,
6 title: 'Post 1',
7 author: { id: 1, name: 'John' },
8 comments: [
9 { id: 1, text: 'Comment 1', author: { id: 2, name: 'Jane' } }
10 ]
11 }
12 ]
13};
14
15// ✅ Normalized structure
16const initialState = {
17 posts: {
18 byId: {
19 1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] }
20 },
21 allIds: [1]
22 },
23 users: {
24 byId: {
25 1: { id: 1, name: 'John' },
26 2: { id: 2, name: 'Jane' }
27 },
28 allIds: [1, 2]
29 },
30 comments: {
31 byId: {
32 1: { id: 1, text: 'Comment 1', authorId: 2, postId: 1 }
33 },
34 allIds: [1]
35 }
36};1// ✅ Memoize components that use Redux
2const TodoItem = React.memo(({ todoId }) => {
3 const todo = useSelector(state => state.todos.byId[todoId]);
4 const dispatch = useDispatch();
5
6 return (
7 <li>
8 <input
9 type="checkbox"
10 checked={todo.completed}
11 onChange={() => dispatch(toggleTodo(todoId))}
12 />
13 {todo.text}
14 </li>
15 );
16});
17
18// Only re-renders when specific todo changes1// Context approach - all consumers re-render
2const AppContext = createContext();
3
4function AppProvider({ children }) {
5 const [users, setUsers] = useState([]);
6 const [posts, setPosts] = useState([]);
7 const [comments, setComments] = useState([]);
8
9 // Any update re-renders ALL consumers
10 const value = { users, setUsers, posts, setPosts, comments, setComments };
11
12 return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
13}
14
15// Redux approach - selective re-renders
16const store = configureStore({
17 reducer: {
18 users: usersSlice.reducer,
19 posts: postsSlice.reducer,
20 comments: commentsSlice.reducer
21 }
22});
23
24function UsersList() {
25 const users = useSelector(state => state.users); // Only re-renders when users change
26 return <ul>{users.map(renderUser)}</ul>;
27}
28
29function PostsList() {
30 const posts = useSelector(state => state.posts); // Only re-renders when posts change
31 return <ul>{posts.map(renderPost)}</ul>;
32}Your progress is saved in this browser, on this device only.