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;
}
}