📅 NGÀY 37: Context API - Advanced Patterns
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu cách kết hợp Context với useReducer cho complex state
- [ ] Nắm vững pattern Custom Provider để encapsulate logic
- [ ] Biết cách compose nhiều Contexts mà không bị "Provider Hell"
- [ ] Tránh được common mistakes khi dùng Context + useReducer
- [ ] Biết khi nào nên tách Context, khi nào nên merge
🤔 Kiểm tra đầu vào (5 phút)
- Context Provider truyền value như thế nào? Value có thể là gì?
- useReducer khác useState ở điểm nào? Khi nào nên dùng useReducer?
- Custom hook cho Context có lợi ích gì? Bắt buộc phải có không?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Từ Ngày 36, chúng ta đã biết Context giải quyết props drilling. Nhưng với complex state, useState trong Provider không đủ:
/**
* ❌ PROBLEM: Complex State với useState
*/
function ShoppingCartProvider({ children }) {
const [items, setItems] = useState([]);
const [discount, setDiscount] = useState(0);
const [shipping, setShipping] = useState('standard');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// ❌ Quá nhiều setState functions
const addItem = (product) => {
setLoading(true);
setItems((prev) => [...prev, product]);
setLoading(false);
};
const applyDiscount = (code) => {
setLoading(true);
// Validate code...
setDiscount(10);
setLoading(false);
};
const changeShipping = (method) => {
setLoading(true);
setShipping(method);
setLoading(false);
};
// ❌ Value object quá lớn
const value = {
items,
setItems,
discount,
setDiscount,
shipping,
setShipping,
loading,
setLoading,
error,
setError,
addItem,
applyDiscount,
changeShipping,
// ... 20 properties nữa!
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
// ❌ Vấn đề:
// - Quá nhiều states riêng lẻ
// - Khó sync states với nhau (items + discount + shipping)
// - Value object recreation → re-render issues
// - Khó maintain, test1.2 Giải Pháp
Context + useReducer - Centralized state với actions:
/**
* ✅ SOLUTION: Context + useReducer
*/
// 1. Define state shape
const initialState = {
items: [],
discount: 0,
shipping: 'standard',
loading: false,
error: null,
};
// 2. Define actions
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
loading: false,
};
case 'APPLY_DISCOUNT':
return {
...state,
discount: action.payload,
loading: false,
};
case 'CHANGE_SHIPPING':
return {
...state,
shipping: action.payload,
loading: false,
};
case 'SET_LOADING':
return {
...state,
loading: action.payload,
};
case 'SET_ERROR':
return {
...state,
error: action.payload,
loading: false,
};
default:
return state;
}
};
// 3. Provider với useReducer
const CartContext = createContext();
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
// ✅ Value object đơn giản
const value = { state, dispatch };
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
// 4. Usage
function AddToCartButton({ product }) {
const { dispatch } = useContext(CartContext);
const handleClick = () => {
dispatch({ type: 'SET_LOADING', payload: true });
dispatch({ type: 'ADD_ITEM', payload: product });
};
return <button onClick={handleClick}>Add to Cart</button>;
}Lợi ích:
- ✅ Centralized state logic
- ✅ Actions rõ ràng, dễ track
- ✅ Easier testing (test reducer riêng)
- ✅ Value object đơn giản hơn
1.3 Mental Model
CONTEXT + useState:
Provider ─┬─ state1 ─┐
├─ state2 ─┤
├─ state3 ─┼─→ { state1, state2, state3, ... }
├─ fn1 ─┤ (10+ properties!)
└─ fn2 ─┘
CONTEXT + useReducer:
Provider ─┬─ state ──┐
└─ dispatch ┼─→ { state, dispatch }
┘ (2 properties only!)
dispatch({ type: 'ACTION' }) ──→ Reducer ──→ New State
Tương tự như: RADIO với REMOTE CONTROL
- Provider = Radio Station
- state = Current song/volume
- dispatch = Remote control buttons
- reducer = Radio's internal logic
→ Nhấn nút (dispatch) → Radio xử lý (reducer) → Kết quả (new state)1.4 Hiểu Lầm Phổ Biến
❌ "Context + useReducer = Redux" → ✅ Giống nhau về pattern, nhưng Context KHÔNG có middleware, DevTools, time-travel
❌ "Luôn dùng useReducer với Context" → ✅ useState vẫn OK cho simple state. useReducer chỉ khi state phức tạp!
❌ "Dispatch là async" → ✅ Dispatch là SYNC! Không được await dispatch()
❌ "Một Context cho toàn bộ app" → ✅ Nên tách nhiều Contexts theo concerns (Auth, Cart, Theme, ...)
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Basic Context + useReducer ⭐
/**
* 🎯 Counter với Context + useReducer
* - Centralized state
* - Multiple actions
*/
import { createContext, useContext, useReducer } from 'react';
// 1. Initial state
const initialState = {
count: 0,
step: 1,
};
// 2. Reducer
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + state.step };
case 'DECREMENT':
return { ...state, count: state.count - state.step };
case 'RESET':
return { ...state, count: 0 };
case 'SET_STEP':
return { ...state, step: action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
// 3. Context
const CounterContext = createContext();
// 4. Provider
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, initialState);
// Note: Chúng ta chưa optimize value object (sẽ học Ngày 38)
const value = { state, dispatch };
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
}
// 5. Custom hook
function useCounter() {
const context = useContext(CounterContext);
if (!context) {
throw new Error('useCounter must be used within CounterProvider');
}
return context;
}
// 6. Components
function CounterDisplay() {
const { state } = useCounter();
return (
<div style={{ fontSize: '48px', textAlign: 'center', margin: '20px' }}>
{state.count}
</div>
);
}
function CounterControls() {
const { state, dispatch } = useCounter();
return (
<div style={{ textAlign: 'center' }}>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>
-{state.step}
</button>
<button
onClick={() => dispatch({ type: 'RESET' })}
style={{ margin: '0 10px' }}
>
Reset
</button>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
+{state.step}
</button>
</div>
);
}
function StepSelector() {
const { state, dispatch } = useCounter();
return (
<div style={{ textAlign: 'center', marginTop: '20px' }}>
<label>
Step size:{' '}
<select
value={state.step}
onChange={(e) =>
dispatch({
type: 'SET_STEP',
payload: Number(e.target.value),
})
}
>
<option value='1'>1</option>
<option value='5'>5</option>
<option value='10'>10</option>
</select>
</label>
</div>
);
}
function App() {
return (
<CounterProvider>
<h1 style={{ textAlign: 'center' }}>Counter với useReducer</h1>
<CounterDisplay />
<CounterControls />
<StepSelector />
</CounterProvider>
);
}
// Result: Counter với step configurable, tất cả state ở một nơiDemo 2: Custom Provider Pattern - Todo App ⭐⭐
/**
* 🎯 Custom Provider với helper functions
* - Encapsulate dispatch logic
* - Cleaner API for consumers
*/
import { createContext, useContext, useReducer } from 'react';
// 1. State & Reducer
const initialState = {
todos: [],
filter: 'all', // 'all' | 'active' | 'completed'
};
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload,
completed: false,
},
],
};
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;
}
};
// 2. Context
const TodoContext = createContext();
// 3. Custom Provider với helper functions
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialState);
// ✅ Helper functions wrap dispatch
// Consumers không cần biết action structure
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
const deleteTodo = (id) => {
dispatch({ type: 'DELETE_TODO', payload: id });
};
const setFilter = (filter) => {
dispatch({ type: 'SET_FILTER', payload: filter });
};
// ✅ Computed values (derived state)
const filteredTodos = state.todos.filter((todo) => {
if (state.filter === 'active') return !todo.completed;
if (state.filter === 'completed') return todo.completed;
return true; // 'all'
});
const stats = {
total: state.todos.length,
active: state.todos.filter((t) => !t.completed).length,
completed: state.todos.filter((t) => t.completed).length,
};
// Value object với clean API
const value = {
todos: filteredTodos,
filter: state.filter,
stats,
addTodo,
toggleTodo,
deleteTodo,
setFilter,
};
return <TodoContext.Provider value={value}>{children}</TodoContext.Provider>;
}
function useTodos() {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodos must be used within TodoProvider');
}
return context;
}
// 4. Components
function TodoInput() {
const { addTodo } = useTodos();
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
addTodo(text);
setText('');
}
};
return (
<form
onSubmit={handleSubmit}
style={{ marginBottom: '20px' }}
>
<input
type='text'
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='What needs to be done?'
style={{ padding: '8px', width: '300px' }}
/>
<button
type='submit'
style={{ marginLeft: '10px', padding: '8px' }}
>
Add
</button>
</form>
);
}
function TodoItem({ todo }) {
const { toggleTodo, deleteTodo } = useTodos();
return (
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '10px',
borderBottom: '1px solid #ddd',
}}
>
<input
type='checkbox'
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
flex: 1,
marginLeft: '10px',
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
);
}
function TodoList() {
const { todos } = useTodos();
if (todos.length === 0) {
return <p>No todos yet!</p>;
}
return (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
/>
))}
</div>
);
}
function TodoFilters() {
const { filter, setFilter, stats } = useTodos();
const filters = ['all', 'active', 'completed'];
return (
<div style={{ marginTop: '20px' }}>
<div>
{filters.map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
style={{
marginRight: '10px',
fontWeight: filter === f ? 'bold' : 'normal',
background: filter === f ? '#007bff' : '#fff',
color: filter === f ? '#fff' : '#000',
padding: '5px 10px',
border: '1px solid #007bff',
cursor: 'pointer',
}}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</button>
))}
</div>
<div style={{ marginTop: '10px', fontSize: '14px', color: '#666' }}>
Total: {stats.total} | Active: {stats.active} | Completed:{' '}
{stats.completed}
</div>
</div>
);
}
function App() {
return (
<TodoProvider>
<div style={{ maxWidth: '500px', margin: '20px auto' }}>
<h1>Todo App</h1>
<TodoInput />
<TodoList />
<TodoFilters />
</div>
</TodoProvider>
);
}
// Result: Clean API - components dùng addTodo(), toggleTodo(), không cần biết dispatchDemo 3: Multiple Contexts Composition ⭐⭐⭐
/**
* 🎯 Multiple Contexts cho different concerns
* - AuthContext: User authentication
* - ThemeContext: UI theme
* - NotificationContext: Toast messages
*/
import { createContext, useContext, useReducer, useState } from 'react';
// ========================================
// AUTH CONTEXT
// ========================================
const AuthContext = createContext();
const authInitialState = {
user: null,
loading: false,
error: null,
};
const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN_START':
return { ...state, loading: true, error: null };
case 'LOGIN_SUCCESS':
return { ...state, loading: false, user: action.payload };
case 'LOGIN_ERROR':
return { ...state, loading: false, error: action.payload };
case 'LOGOUT':
return { ...state, user: null, error: null };
default:
return state;
}
};
function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, authInitialState);
const login = async (email, password) => {
dispatch({ type: 'LOGIN_START' });
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
if (email === 'admin@test.com' && password === '123') {
dispatch({
type: 'LOGIN_SUCCESS',
payload: { email, name: 'Admin User' },
});
return true;
} else {
throw new Error('Invalid credentials');
}
} catch (error) {
dispatch({ type: 'LOGIN_ERROR', payload: error.message });
return false;
}
};
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
const value = {
user: state.user,
loading: state.loading,
error: state.error,
login,
logout,
isAuthenticated: !!state.user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
// ========================================
// THEME CONTEXT
// ========================================
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
const value = { theme, toggleTheme };
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}
// ========================================
// NOTIFICATION CONTEXT
// ========================================
const NotificationContext = createContext();
function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const addNotification = (message, type = 'info') => {
const id = Date.now();
const notification = { id, message, type };
setNotifications((prev) => [...prev, notification]);
// Auto remove after 3 seconds
setTimeout(() => {
removeNotification(id);
}, 3000);
};
const removeNotification = (id) => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
};
const value = {
notifications,
addNotification,
removeNotification,
};
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
);
}
function useNotifications() {
const context = useContext(NotificationContext);
if (!context)
throw new Error(
'useNotifications must be used within NotificationProvider',
);
return context;
}
// ========================================
// APP PROVIDERS (Composition)
// ========================================
function AppProviders({ children }) {
return (
<ThemeProvider>
<AuthProvider>
<NotificationProvider>{children}</NotificationProvider>
</AuthProvider>
</ThemeProvider>
);
}
// ========================================
// COMPONENTS
// ========================================
function NotificationList() {
const { notifications, removeNotification } = useNotifications();
if (notifications.length === 0) return null;
return (
<div
style={{
position: 'fixed',
top: '20px',
right: '20px',
zIndex: 1000,
}}
>
{notifications.map((notif) => (
<div
key={notif.id}
style={{
padding: '10px 15px',
marginBottom: '10px',
background:
notif.type === 'error'
? '#f44336'
: notif.type === 'success'
? '#4caf50'
: '#2196f3',
color: 'white',
borderRadius: '4px',
minWidth: '200px',
}}
>
{notif.message}
<button
onClick={() => removeNotification(notif.id)}
style={{
float: 'right',
background: 'transparent',
border: 'none',
color: 'white',
cursor: 'pointer',
}}
>
✕
</button>
</div>
))}
</div>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
const { user, logout } = useAuth();
return (
<header
style={{
padding: '15px',
background: theme === 'light' ? '#f5f5f5' : '#333',
color: theme === 'light' ? '#000' : '#fff',
borderBottom: '2px solid ' + (theme === 'light' ? '#ddd' : '#555'),
}}
>
<h1 style={{ display: 'inline' }}>My App</h1>
<div style={{ float: 'right' }}>
<button
onClick={toggleTheme}
style={{ marginRight: '10px' }}
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
{user && (
<>
<span style={{ marginRight: '10px' }}>{user.name}</span>
<button onClick={logout}>Logout</button>
</>
)}
</div>
</header>
);
}
function LoginForm() {
const { login, loading, error } = useAuth();
const { addNotification } = useNotifications();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const success = await login(email, password);
if (success) {
addNotification('Login successful!', 'success');
} else {
addNotification('Login failed!', 'error');
}
};
return (
<form
onSubmit={handleSubmit}
style={{ maxWidth: '300px', margin: '20px auto' }}
>
<h2>Login</h2>
{error && (
<div style={{ color: 'red', marginBottom: '10px' }}>{error}</div>
)}
<div style={{ marginBottom: '10px' }}>
<input
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder='Email'
style={{ width: '100%', padding: '8px' }}
disabled={loading}
/>
</div>
<div style={{ marginBottom: '10px' }}>
<input
type='password'
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder='Password'
style={{ width: '100%', padding: '8px' }}
disabled={loading}
/>
</div>
<button
type='submit'
disabled={loading}
style={{ width: '100%', padding: '10px' }}
>
{loading ? 'Logging in...' : 'Login'}
</button>
<p style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}>
Hint: admin@test.com / 123
</p>
</form>
);
}
function Dashboard() {
const { user } = useAuth();
const { theme } = useTheme();
const { addNotification } = useNotifications();
return (
<div
style={{
padding: '20px',
background: theme === 'light' ? '#fff' : '#222',
color: theme === 'light' ? '#000' : '#fff',
minHeight: '300px',
}}
>
<h2>Welcome, {user.name}!</h2>
<p>This is your dashboard.</p>
<button onClick={() => addNotification('Test notification', 'info')}>
Show Test Notification
</button>
</div>
);
}
function App() {
const { isAuthenticated } = useAuth();
return (
<div>
<NotificationList />
<Header />
{isAuthenticated ? <Dashboard /> : <LoginForm />}
</div>
);
}
function Root() {
return (
<AppProviders>
<App />
</AppProviders>
);
}
/**
* Result: 3 contexts hoạt động độc lập
* - Auth quản lý user state
* - Theme quản lý UI
* - Notifications quản lý toasts
*
* Login → Show success notification
* Toggle theme → UI changes
* Logout → Back to login
*/🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: Basic Context + useReducer (15 phút)
/**
* 🎯 Mục tiêu: Áp dụng Context + useReducer cơ bản
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: Multiple contexts, advanced patterns
*
* Requirements:
* 1. Tạo FormContext với useReducer
* 2. State: { name: '', email: '', age: '' }
* 3. Actions: UPDATE_FIELD, RESET_FORM
* 4. FormInput component để nhập data
* 5. FormPreview component để hiển thị data
* 6. Reset button
*
* 💡 Gợi ý:
* - Action payload: { field: 'name', value: 'John' }
* - Reducer dùng computed property name: [action.payload.field]
*/
// ❌ Cách SAI:
// - Dùng useState thay vì useReducer
// - Không có action types (dùng string trực tiếp)
// - Không validate action.type trong reducer
// ✅ Cách ĐÚNG: Xem solution
// 🎯 NHIỆM VỤ:
// TODO: Implement FormContext với useReducer
// TODO: Implement reducer với UPDATE_FIELD, RESET_FORM
// TODO: Implement FormInput (3 fields: name, email, age)
// TODO: Implement FormPreview
// TODO: Reset button clears all fields💡 Solution
import { createContext, useContext, useReducer } from 'react';
/**
* Form Context với useReducer
* Quản lý form state centralized
*/
// 1. Initial state
const initialState = {
name: '',
email: '',
age: '',
};
// 2. Reducer
const formReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.payload.field]: action.payload.value,
};
case 'RESET_FORM':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
// 3. Context
const FormContext = createContext();
function FormProvider({ children }) {
const [state, dispatch] = useReducer(formReducer, initialState);
const value = { state, dispatch };
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
}
function useForm() {
const context = useContext(FormContext);
if (!context) {
throw new Error('useForm must be used within FormProvider');
}
return context;
}
// 4. Components
function FormInput() {
const { state, dispatch } = useForm();
const handleChange = (field, value) => {
dispatch({
type: 'UPDATE_FIELD',
payload: { field, value },
});
};
return (
<div style={{ marginBottom: '20px' }}>
<h3>Form Input</h3>
<div style={{ marginBottom: '10px' }}>
<label>
Name:{' '}
<input
type='text'
value={state.name}
onChange={(e) => handleChange('name', e.target.value)}
style={{ padding: '5px' }}
/>
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
Email:{' '}
<input
type='email'
value={state.email}
onChange={(e) => handleChange('email', e.target.value)}
style={{ padding: '5px' }}
/>
</label>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
Age:{' '}
<input
type='number'
value={state.age}
onChange={(e) => handleChange('age', e.target.value)}
style={{ padding: '5px' }}
/>
</label>
</div>
<button onClick={() => dispatch({ type: 'RESET_FORM' })}>
Reset Form
</button>
</div>
);
}
function FormPreview() {
const { state } = useForm();
const isEmpty = !state.name && !state.email && !state.age;
return (
<div
style={{
padding: '15px',
background: '#f5f5f5',
borderRadius: '4px',
}}
>
<h3>Form Preview</h3>
{isEmpty ? (
<p>No data yet...</p>
) : (
<div>
{state.name && (
<p>
<strong>Name:</strong> {state.name}
</p>
)}
{state.email && (
<p>
<strong>Email:</strong> {state.email}
</p>
)}
{state.age && (
<p>
<strong>Age:</strong> {state.age}
</p>
)}
</div>
)}
</div>
);
}
function App() {
return (
<FormProvider>
<div style={{ maxWidth: '500px', margin: '20px auto' }}>
<h1>Form Demo</h1>
<FormInput />
<FormPreview />
</div>
</FormProvider>
);
}
// Result: Type trong form → Preview updates real-time. Reset → Clear all fields⭐⭐ Level 2: Custom Provider Pattern (25 phút)
/**
* 🎯 Mục tiêu: Encapsulate logic trong Custom Provider
* ⏱️ Thời gian: 25 phút
*
* Scenario: Shopping Cart với complex operations
* - Add item (nếu đã có thì tăng quantity)
* - Remove item
* - Update quantity
* - Clear cart
* - Calculate totals
*
* 🤔 PHÂN TÍCH:
*
* Approach A: Expose dispatch trực tiếp
* Pros: Flexible, components tự dispatch
* Cons: Components phải biết action structure
* Khó maintain nếu action thay đổi
*
* Approach B: Custom Provider với helper functions
* Pros: Clean API, encapsulate logic
* Components không biết về dispatch
* Cons: Nhiều code hơn (wrapper functions)
*
* 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
*
* Requirements:
* 1. CartContext với useReducer
* 2. Helper functions: addItem, removeItem, updateQuantity, clearCart
* 3. Computed values: totalItems, totalPrice
* 4. Components dùng helper functions, KHÔNG dùng dispatch
*/💡 Solution
import { createContext, useContext, useReducer } from 'react';
/**
* DECISION: Chọn Approach B - Custom Provider với helper functions
*
* RATIONALE:
* - Cleaner API cho components
* - Logic centralized trong Provider
* - Dễ refactor actions mà không ảnh hưởng components
* - Better for team collaboration (clear API contract)
*/
// 1. Initial state & reducer
const initialState = {
items: [],
// items: [{ id, name, price, quantity }]
};
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM': {
const existingIndex = state.items.findIndex(
(item) => item.id === action.payload.id,
);
if (existingIndex >= 0) {
// Item exists, increase quantity
const newItems = [...state.items];
newItems[existingIndex] = {
...newItems[existingIndex],
quantity: newItems[existingIndex].quantity + 1,
};
return { ...state, items: newItems };
} else {
// New 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': {
const { id, quantity } = action.payload;
if (quantity <= 0) {
// Remove if quantity is 0
return {
...state,
items: state.items.filter((item) => item.id !== id),
};
}
return {
...state,
items: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item,
),
};
}
case 'CLEAR_CART':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
// 2. Context
const CartContext = createContext();
// 3. Custom Provider
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
// Helper functions
const addItem = (product) => {
dispatch({
type: 'ADD_ITEM',
payload: product,
});
};
const removeItem = (productId) => {
dispatch({
type: 'REMOVE_ITEM',
payload: productId,
});
};
const updateQuantity = (productId, quantity) => {
dispatch({
type: 'UPDATE_QUANTITY',
payload: { id: productId, quantity },
});
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
// Computed values
const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
// Clean API
const value = {
items: state.items,
totalItems,
totalPrice,
addItem,
removeItem,
updateQuantity,
clearCart,
};
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;
}
// 4. Components
function ProductCard({ product }) {
const { addItem } = useCart();
return (
<div
style={{
border: '1px solid #ddd',
padding: '15px',
margin: '10px',
borderRadius: '4px',
}}
>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>Add to Cart</button>
</div>
);
}
function ProductList() {
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Keyboard', price: 79 },
{ id: 4, name: 'Monitor', price: 399 },
];
return (
<div>
<h2>Products</h2>
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
</div>
);
}
function CartItem({ item }) {
const { removeItem, updateQuantity } = useCart();
return (
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '10px',
borderBottom: '1px solid #ddd',
}}
>
<div style={{ flex: 1 }}>
<strong>{item.name}</strong>
<div style={{ fontSize: '14px', color: '#666' }}>
${item.price} each
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span style={{ minWidth: '30px', textAlign: 'center' }}>
{item.quantity}
</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
<div style={{ marginLeft: '20px', fontWeight: 'bold' }}>
${item.price * item.quantity}
</div>
</div>
);
}
function Cart() {
const { items, totalItems, totalPrice, clearCart } = useCart();
if (items.length === 0) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<p>Your cart is empty</p>
</div>
);
}
return (
<div style={{ padding: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '15px',
}}
>
<h2>Shopping Cart ({totalItems} items)</h2>
<button onClick={clearCart}>Clear Cart</button>
</div>
{items.map((item) => (
<CartItem
key={item.id}
item={item}
/>
))}
<div
style={{
marginTop: '20px',
padding: '15px',
background: '#f5f5f5',
borderRadius: '4px',
}}
>
<h3>Total: ${totalPrice.toFixed(2)}</h3>
<button
style={{
padding: '10px 20px',
background: '#28a745',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Checkout
</button>
</div>
</div>
);
}
function App() {
return (
<CartProvider>
<div style={{ maxWidth: '800px', margin: '20px auto' }}>
<h1>Shopping Cart Demo</h1>
<ProductList />
<Cart />
</div>
</CartProvider>
);
}
/**
* Result:
* - Add product → Quantity increases if exists
* - +/- buttons → Update quantity
* - Remove → Delete item
* - Clear Cart → Empty all items
* - Components dùng clean API (addItem, removeItem, etc)
*/⭐⭐⭐ Level 3: Multi-Step Form với Context (40 phút)
/**
* 🎯 Mục tiêu: Complex state machine với useReducer
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là user, tôi muốn điền form đăng ký qua nhiều bước
* để không bị overwhelm bởi quá nhiều fields"
*
* ✅ Acceptance Criteria:
* - [ ] 3 steps: Personal Info → Account Info → Confirmation
* - [ ] Next/Previous navigation
* - [ ] Data persist khi navigate giữa steps
* - [ ] Validation mỗi step trước khi Next
* - [ ] Progress indicator
* - [ ] Submit ở step cuối
*
* 🎨 Technical Constraints:
* - Context + useReducer cho state management
* - Custom Provider với navigation helpers
* - Validation logic trong reducer
*
* 🚨 Edge Cases cần handle:
* - Cannot go Next nếu validation fails
* - Cannot go Previous từ step 1
* - Cannot submit nếu chưa complete tất cả steps
*
* 📝 Implementation Checklist:
* - [ ] FormContext với useReducer
* - [ ] Actions: UPDATE_FIELD, NEXT_STEP, PREV_STEP, SUBMIT
* - [ ] 3 step components
* - [ ] Navigation buttons
* - [ ] Progress bar
*/💡 Solution
import { createContext, useContext, useReducer } from 'react';
/**
* Multi-Step Registration Form
* Steps: Personal → Account → Confirmation
*/
// 1. Initial state
const initialState = {
currentStep: 1,
totalSteps: 3,
data: {
// Step 1: Personal Info
firstName: '',
lastName: '',
email: '',
// Step 2: Account Info
username: '',
password: '',
confirmPassword: '',
// Step 3: Confirmation
agreeToTerms: false,
},
errors: {},
isSubmitted: false,
};
// 2. Validation functions
const validateStep1 = (data) => {
const errors = {};
if (!data.firstName.trim()) {
errors.firstName = 'First name is required';
}
if (!data.lastName.trim()) {
errors.lastName = 'Last name is required';
}
if (!data.email.trim()) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(data.email)) {
errors.email = 'Email is invalid';
}
return errors;
};
const validateStep2 = (data) => {
const errors = {};
if (!data.username.trim()) {
errors.username = 'Username is required';
} else if (data.username.length < 3) {
errors.username = 'Username must be at least 3 characters';
}
if (!data.password) {
errors.password = 'Password is required';
} else if (data.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
if (data.password !== data.confirmPassword) {
errors.confirmPassword = 'Passwords do not match';
}
return errors;
};
const validateStep3 = (data) => {
const errors = {};
if (!data.agreeToTerms) {
errors.agreeToTerms = 'You must agree to terms';
}
return errors;
};
// 3. Reducer
const formReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
data: {
...state.data,
[action.payload.field]: action.payload.value,
},
errors: {
...state.errors,
[action.payload.field]: undefined, // Clear error for this field
},
};
case 'NEXT_STEP': {
// Validate current step
let errors = {};
if (state.currentStep === 1) {
errors = validateStep1(state.data);
} else if (state.currentStep === 2) {
errors = validateStep2(state.data);
}
if (Object.keys(errors).length > 0) {
return {
...state,
errors,
};
}
// Validation passed, go to next step
return {
...state,
currentStep: state.currentStep + 1,
errors: {},
};
}
case 'PREV_STEP':
return {
...state,
currentStep: Math.max(1, state.currentStep - 1),
errors: {},
};
case 'SUBMIT': {
// Final validation
const errors = validateStep3(state.data);
if (Object.keys(errors).length > 0) {
return {
...state,
errors,
};
}
// Submit successful
return {
...state,
isSubmitted: true,
errors: {},
};
}
default:
return state;
}
};
// 4. Context
const FormContext = createContext();
function FormProvider({ children }) {
const [state, dispatch] = useReducer(formReducer, initialState);
const updateField = (field, value) => {
dispatch({
type: 'UPDATE_FIELD',
payload: { field, value },
});
};
const nextStep = () => {
dispatch({ type: 'NEXT_STEP' });
};
const prevStep = () => {
dispatch({ type: 'PREV_STEP' });
};
const submit = () => {
dispatch({ type: 'SUBMIT' });
};
const value = {
...state,
updateField,
nextStep,
prevStep,
submit,
};
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
}
function useFormContext() {
const context = useContext(FormContext);
if (!context) {
throw new Error('useFormContext must be used within FormProvider');
}
return context;
}
// 5. Components
function ProgressBar() {
const { currentStep, totalSteps } = useFormContext();
const progress = (currentStep / totalSteps) * 100;
return (
<div style={{ marginBottom: '30px' }}>
<div
style={{
height: '8px',
background: '#e0e0e0',
borderRadius: '4px',
overflow: 'hidden',
}}
>
<div
style={{
height: '100%',
width: `${progress}%`,
background: '#4caf50',
transition: 'width 0.3s',
}}
/>
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '10px',
fontSize: '14px',
color: '#666',
}}
>
<span>Personal Info</span>
<span>Account Info</span>
<span>Confirmation</span>
</div>
</div>
);
}
function Step1() {
const { data, errors, updateField } = useFormContext();
return (
<div>
<h2>Step 1: Personal Information</h2>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
First Name *
</label>
<input
type='text'
value={data.firstName}
onChange={(e) => updateField('firstName', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: errors.firstName ? '1px solid red' : '1px solid #ccc',
}}
/>
{errors.firstName && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
{errors.firstName}
</div>
)}
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
Last Name *
</label>
<input
type='text'
value={data.lastName}
onChange={(e) => updateField('lastName', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: errors.lastName ? '1px solid red' : '1px solid #ccc',
}}
/>
{errors.lastName && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
{errors.lastName}
</div>
)}
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>Email *</label>
<input
type='email'
value={data.email}
onChange={(e) => updateField('email', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: errors.email ? '1px solid red' : '1px solid #ccc',
}}
/>
{errors.email && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
{errors.email}
</div>
)}
</div>
</div>
);
}
function Step2() {
const { data, errors, updateField } = useFormContext();
return (
<div>
<h2>Step 2: Account Information</h2>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
Username *
</label>
<input
type='text'
value={data.username}
onChange={(e) => updateField('username', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: errors.username ? '1px solid red' : '1px solid #ccc',
}}
/>
{errors.username && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
{errors.username}
</div>
)}
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
Password *
</label>
<input
type='password'
value={data.password}
onChange={(e) => updateField('password', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: errors.password ? '1px solid red' : '1px solid #ccc',
}}
/>
{errors.password && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
{errors.password}
</div>
)}
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}>
Confirm Password *
</label>
<input
type='password'
value={data.confirmPassword}
onChange={(e) => updateField('confirmPassword', e.target.value)}
style={{
width: '100%',
padding: '8px',
border: errors.confirmPassword ? '1px solid red' : '1px solid #ccc',
}}
/>
{errors.confirmPassword && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
{errors.confirmPassword}
</div>
)}
</div>
</div>
);
}
function Step3() {
const { data, errors, updateField } = useFormContext();
return (
<div>
<h2>Step 3: Confirmation</h2>
<div
style={{
padding: '15px',
background: '#f5f5f5',
borderRadius: '4px',
marginBottom: '20px',
}}
>
<h3>Please review your information:</h3>
<p>
<strong>Name:</strong> {data.firstName} {data.lastName}
</p>
<p>
<strong>Email:</strong> {data.email}
</p>
<p>
<strong>Username:</strong> {data.username}
</p>
</div>
<div style={{ marginBottom: '15px' }}>
<label>
<input
type='checkbox'
checked={data.agreeToTerms}
onChange={(e) => updateField('agreeToTerms', e.target.checked)}
/>{' '}
I agree to the terms and conditions *
</label>
{errors.agreeToTerms && (
<div style={{ color: 'red', fontSize: '14px', marginTop: '5px' }}>
{errors.agreeToTerms}
</div>
)}
</div>
</div>
);
}
function Navigation() {
const { currentStep, totalSteps, nextStep, prevStep, submit } =
useFormContext();
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #ddd',
}}
>
<button
onClick={prevStep}
disabled={currentStep === 1}
style={{
padding: '10px 20px',
cursor: currentStep === 1 ? 'not-allowed' : 'pointer',
opacity: currentStep === 1 ? 0.5 : 1,
}}
>
Previous
</button>
{currentStep < totalSteps ? (
<button
onClick={nextStep}
style={{
padding: '10px 20px',
background: '#2196f3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Next
</button>
) : (
<button
onClick={submit}
style={{
padding: '10px 20px',
background: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Submit
</button>
)}
</div>
);
}
function FormContent() {
const { currentStep, isSubmitted, data } = useFormContext();
if (isSubmitted) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<h2 style={{ color: '#4caf50' }}>✓ Registration Successful!</h2>
<p>Welcome, {data.firstName}!</p>
<p>A confirmation email has been sent to {data.email}</p>
</div>
);
}
return (
<div>
<ProgressBar />
{currentStep === 1 && <Step1 />}
{currentStep === 2 && <Step2 />}
{currentStep === 3 && <Step3 />}
<Navigation />
</div>
);
}
function App() {
return (
<FormProvider>
<div
style={{
maxWidth: '600px',
margin: '40px auto',
padding: '30px',
border: '1px solid #ddd',
borderRadius: '8px',
}}
>
<h1 style={{ textAlign: 'center', marginBottom: '30px' }}>
Registration Form
</h1>
<FormContent />
</div>
</FormProvider>
);
}
/**
* Result:
* - Step 1: Enter personal info → Next validates → Go to Step 2
* - Step 2: Enter account info → Next validates → Go to Step 3
* - Step 3: Review → Check terms → Submit
* - Success message shows after submit
*
* Edge cases handled:
* - Validation errors prevent navigation
* - Previous disabled on Step 1
* - Data persists when navigating back/forward
*/⭐⭐⭐⭐ Level 4: Undo/Redo với Context + useReducer (60 phút)
/**
* 🎯 Mục tiêu: State machine với history tracking
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Nhiệm vụ:
* 1. So sánh approaches cho undo/redo:
* - Store full history array
* - Store diffs only
* - Command pattern
*
* 2. Document pros/cons
*
* 3. Chọn approach
*
* 4. Viết ADR
*
* ADR Template:
* - Context: Canvas app cần undo/redo cho drawing actions
* - Decision: Approach đã chọn
* - Rationale: Tại sao
* - Consequences: Trade-offs
* - Alternatives: Các options khác
*
* 💻 PHASE 2: Implementation (30 phút)
*
* Requirements:
* - DrawingContext với useReducer
* - Actions: ADD_SHAPE, UNDO, REDO, CLEAR
* - History tracking (past/present/future)
* - Canvas hiển thị shapes
* - Undo/Redo buttons
* - Max history: 20 items
*
* 🧪 PHASE 3: Testing (10 phút)
* - [ ] Add shapes → History grows
* - [ ] Undo → Restore previous state
* - [ ] Redo → Restore next state
* - [ ] Add after undo → Clear future
* - [ ] Max history respected
*/💡 Solution
import { createContext, useContext, useReducer } from 'react';
/**
* ADR: Undo/Redo Implementation
*
* CONTEXT:
* Drawing app cần undo/redo functionality
* Users cần revert/restore drawing actions
*
* DECISION: Full History Array (past/present/future)
*
* RATIONALE:
* 1. Simplicity: Dễ implement và understand
* 2. Immutability: Mỗi state là snapshot độc lập
* 3. Performance: OK cho simple shapes (not thousands)
* 4. Debugging: Dễ inspect history
*
* CONSEQUENCES (Trade-offs):
* - Memory: Lưu full state mỗi step (acceptable cho simple data)
* - Max history limit: 20 items để prevent memory issues
* - Không optimal cho large/complex state
*
* ALTERNATIVES CONSIDERED:
* 1. Store diffs only: Memory efficient, nhưng complex implementation
* 2. Command pattern: Flexible, nhưng overkill cho use case này
*/
// 1. Initial state
const initialState = {
past: [],
present: [],
future: [],
};
const MAX_HISTORY = 20;
// 2. Reducer
const drawingReducer = (state, action) => {
switch (action.type) {
case 'ADD_SHAPE': {
const newPresent = [...state.present, action.payload];
// Add current state to past
const newPast = [...state.past, state.present];
// Limit history size
const limitedPast = newPast.slice(-MAX_HISTORY);
return {
past: limitedPast,
present: newPresent,
future: [], // Clear future when new action
};
}
case 'UNDO': {
if (state.past.length === 0) {
return state; // Nothing to undo
}
const previous = state.past[state.past.length - 1];
const newPast = state.past.slice(0, -1);
return {
past: newPast,
present: previous,
future: [state.present, ...state.future],
};
}
case 'REDO': {
if (state.future.length === 0) {
return state; // Nothing to redo
}
const next = state.future[0];
const newFuture = state.future.slice(1);
return {
past: [...state.past, state.present],
present: next,
future: newFuture,
};
}
case 'CLEAR':
return initialState;
default:
return state;
}
};
// 3. Context
const DrawingContext = createContext();
function DrawingProvider({ children }) {
const [state, dispatch] = useReducer(drawingReducer, initialState);
const addShape = (shape) => {
dispatch({ type: 'ADD_SHAPE', payload: shape });
};
const undo = () => {
dispatch({ type: 'UNDO' });
};
const redo = () => {
dispatch({ type: 'REDO' });
};
const clear = () => {
dispatch({ type: 'CLEAR' });
};
const value = {
shapes: state.present,
canUndo: state.past.length > 0,
canRedo: state.future.length > 0,
historySize: state.past.length,
addShape,
undo,
redo,
clear,
};
return (
<DrawingContext.Provider value={value}>{children}</DrawingContext.Provider>
);
}
function useDrawing() {
const context = useContext(DrawingContext);
if (!context) {
throw new Error('useDrawing must be used within DrawingProvider');
}
return context;
}
// 4. Components
function Toolbar() {
const { canUndo, canRedo, undo, redo, clear, addShape } = useDrawing();
const addCircle = () => {
addShape({
id: Date.now(),
type: 'circle',
x: Math.random() * 400,
y: Math.random() * 300,
radius: 20 + Math.random() * 30,
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
});
};
const addSquare = () => {
addShape({
id: Date.now(),
type: 'square',
x: Math.random() * 400,
y: Math.random() * 300,
size: 30 + Math.random() * 40,
color: `hsl(${Math.random() * 360}, 70%, 50%)`,
});
};
return (
<div
style={{
marginBottom: '20px',
padding: '15px',
background: '#f5f5f5',
borderRadius: '4px',
display: 'flex',
gap: '10px',
flexWrap: 'wrap',
}}
>
<button
onClick={addCircle}
style={{ padding: '8px 15px' }}
>
Add Circle
</button>
<button
onClick={addSquare}
style={{ padding: '8px 15px' }}
>
Add Square
</button>
<div style={{ flex: 1 }} />
<button
onClick={undo}
disabled={!canUndo}
style={{
padding: '8px 15px',
cursor: canUndo ? 'pointer' : 'not-allowed',
opacity: canUndo ? 1 : 0.5,
}}
>
↶ Undo
</button>
<button
onClick={redo}
disabled={!canRedo}
style={{
padding: '8px 15px',
cursor: canRedo ? 'pointer' : 'not-allowed',
opacity: canRedo ? 1 : 0.5,
}}
>
↷ Redo
</button>
<button
onClick={clear}
style={{ padding: '8px 15px' }}
>
Clear
</button>
</div>
);
}
function Canvas() {
const { shapes } = useDrawing();
return (
<svg
width='500'
height='400'
style={{
border: '2px solid #333',
borderRadius: '4px',
background: 'white',
}}
>
{shapes.map((shape) => {
if (shape.type === 'circle') {
return (
<circle
key={shape.id}
cx={shape.x}
cy={shape.y}
r={shape.radius}
fill={shape.color}
/>
);
} else if (shape.type === 'square') {
return (
<rect
key={shape.id}
x={shape.x}
y={shape.y}
width={shape.size}
height={shape.size}
fill={shape.color}
/>
);
}
return null;
})}
</svg>
);
}
function HistoryInfo() {
const { shapes, historySize, canUndo, canRedo } = useDrawing();
return (
<div
style={{
marginTop: '15px',
padding: '10px',
background: '#e3f2fd',
borderRadius: '4px',
fontSize: '14px',
}}
>
<div>
<strong>Shapes:</strong> {shapes.length}
</div>
<div>
<strong>History size:</strong> {historySize} (max: {MAX_HISTORY})
</div>
<div>
<strong>Can undo:</strong> {canUndo ? 'Yes' : 'No'}
</div>
<div>
<strong>Can redo:</strong> {canRedo ? 'Yes' : 'No'}
</div>
</div>
);
}
function App() {
return (
<DrawingProvider>
<div style={{ maxWidth: '600px', margin: '20px auto' }}>
<h1>Drawing App với Undo/Redo</h1>
<Toolbar />
<Canvas />
<HistoryInfo />
<div
style={{
marginTop: '20px',
padding: '15px',
background: '#fff3cd',
borderRadius: '4px',
fontSize: '14px',
}}
>
<strong>Instructions:</strong>
<ul style={{ marginTop: '10px', marginBottom: 0 }}>
<li>Click "Add Circle" or "Add Square" to add shapes</li>
<li>Click "Undo" to remove last shape</li>
<li>Click "Redo" to restore undone shape</li>
<li>Adding new shape after undo clears redo history</li>
<li>History limited to {MAX_HISTORY} items</li>
</ul>
</div>
</div>
</DrawingProvider>
);
}
/**
* TESTING CHECKLIST:
* ✅ Add shapes → Shapes appear on canvas
* ✅ Undo → Last shape disappears
* ✅ Redo → Shape reappears
* ✅ Add after undo → Redo history cleared
* ✅ Max history → Old states removed
* ✅ Undo button disabled when no history
* ✅ Redo button disabled when no future
*
* PERFORMANCE NOTE:
* - For complex apps với large state, consider:
* - Structural sharing (Immer-style)
* - Diff-based history
* - Command pattern
* - Current approach OK cho simple shapes
*/⭐⭐⭐⭐⭐ Level 5: Production State Management System (90 phút)
/**
* 🎯 Mục tiêu: Production-grade context architecture
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
* E-commerce app với complex state management:
* - Auth (user, login/logout)
* - Cart (items, add/remove/update)
* - Products (list, filter, search)
* - UI (theme, notifications)
*
* 🏗️ Technical Design Doc:
*
* 1. Component Architecture:
* - Separate contexts cho mỗi concern
* - AppProviders composition
* - Custom hooks cho mỗi context
*
* 2. State Management Strategy:
* - useReducer cho complex state (Cart, Products)
* - useState cho simple state (Theme)
* - Derived state computation
*
* 3. Performance Considerations:
* - Note về optimization (sẽ làm Ngày 38)
* - Document known performance issues
*
* 4. Error Handling:
* - Error boundaries
* - Loading states
* - Network error handling
*
* ✅ Production Checklist:
* - [ ] 4 separate contexts
* - [ ] useReducer cho Cart & Products
* - [ ] Helper functions encapsulate dispatch
* - [ ] Custom hooks với error checks
* - [ ] Loading & error states
* - [ ] Comments document architecture
*
* 📝 Documentation:
* - Architecture overview
* - Context responsibilities
* - Usage examples
*/💡 Solution
import {
createContext,
useContext,
useReducer,
useState,
useEffect,
} from 'react';
/**
* PRODUCTION STATE MANAGEMENT ARCHITECTURE
*
* STRUCTURE:
* - AuthContext: User authentication & authorization
* - CartContext: Shopping cart management
* - ProductContext: Product catalog & filtering
* - UIContext: Theme, notifications, modals
*
* PRINCIPLES:
* - Separation of concerns
* - Single responsibility per context
* - Custom hooks encapsulate logic
* - Helper functions hide dispatch complexity
*
* NOTE: This is NOT optimized for performance yet.
* We'll learn optimization patterns in Day 38.
* Known issues:
* - Context value object recreation
* - Unnecessary re-renders
* These are acceptable for learning purposes.
*/
// ========================================
// CART CONTEXT
// ========================================
const CartContext = createContext();
const cartInitialState = {
items: [],
loading: false,
error: 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: [] };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
default:
return state;
}
};
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, cartInitialState);
const addItem = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
const removeItem = (productId) => {
dispatch({ type: 'REMOVE_ITEM', payload: productId });
};
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeItem(productId);
} else {
dispatch({
type: 'UPDATE_QUANTITY',
payload: { id: productId, quantity },
});
}
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
// Computed values
const totalItems = state.items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
const value = {
items: state.items,
loading: state.loading,
error: state.error,
totalItems,
totalPrice,
addItem,
removeItem,
updateQuantity,
clearCart,
};
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;
}
// ========================================
// PRODUCT CONTEXT
// ========================================
const ProductContext = createContext();
const productInitialState = {
products: [],
filteredProducts: [],
searchQuery: '',
category: 'all',
loading: false,
error: null,
};
const productReducer = (state, action) => {
switch (action.type) {
case 'SET_PRODUCTS':
return {
...state,
products: action.payload,
filteredProducts: action.payload,
loading: false,
};
case 'SET_SEARCH_QUERY': {
const query = action.payload.toLowerCase();
const filtered = state.products.filter(
(p) =>
p.name.toLowerCase().includes(query) &&
(state.category === 'all' || p.category === state.category),
);
return {
...state,
searchQuery: action.payload,
filteredProducts: filtered,
};
}
case 'SET_CATEGORY': {
const filtered = state.products.filter(
(p) =>
(action.payload === 'all' || p.category === action.payload) &&
(state.searchQuery === '' ||
p.name.toLowerCase().includes(state.searchQuery.toLowerCase())),
);
return {
...state,
category: action.payload,
filteredProducts: filtered,
};
}
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
default:
return state;
}
};
function ProductProvider({ children }) {
const [state, dispatch] = useReducer(productReducer, productInitialState);
// Simulate API fetch
useEffect(() => {
dispatch({ type: 'SET_LOADING', payload: true });
setTimeout(() => {
const mockProducts = [
{ id: 1, name: 'Laptop Pro', price: 1299, category: 'electronics' },
{ id: 2, name: 'Wireless Mouse', price: 29, category: 'electronics' },
{ id: 3, name: 'Desk Chair', price: 199, category: 'furniture' },
{ id: 4, name: 'Standing Desk', price: 499, category: 'furniture' },
{ id: 5, name: 'Monitor 27"', price: 399, category: 'electronics' },
{
id: 6,
name: 'Keyboard Mechanical',
price: 129,
category: 'electronics',
},
];
dispatch({ type: 'SET_PRODUCTS', payload: mockProducts });
}, 500);
}, []);
const setSearchQuery = (query) => {
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
};
const setCategory = (category) => {
dispatch({ type: 'SET_CATEGORY', payload: category });
};
const value = {
products: state.filteredProducts,
allProducts: state.products,
searchQuery: state.searchQuery,
category: state.category,
loading: state.loading,
error: state.error,
setSearchQuery,
setCategory,
};
return (
<ProductContext.Provider value={value}>{children}</ProductContext.Provider>
);
}
function useProducts() {
const context = useContext(ProductContext);
if (!context)
throw new Error('useProducts must be used within ProductProvider');
return context;
}
// ========================================
// UI CONTEXT
// ========================================
const UIContext = createContext();
function UIProvider({ children }) {
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState([]);
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
const addNotification = (message, type = 'info') => {
const id = Date.now();
setNotifications((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
removeNotification(id);
}, 3000);
};
const removeNotification = (id) => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
};
const value = {
theme,
toggleTheme,
notifications,
addNotification,
removeNotification,
};
return <UIContext.Provider value={value}>{children}</UIContext.Provider>;
}
function useUI() {
const context = useContext(UIContext);
if (!context) throw new Error('useUI must be used within UIProvider');
return context;
}
// ========================================
// APP PROVIDERS COMPOSITION
// ========================================
function AppProviders({ children }) {
return (
<UIProvider>
<ProductProvider>
<CartProvider>{children}</CartProvider>
</ProductProvider>
</UIProvider>
);
}
// ========================================
// COMPONENTS
// ========================================
function Header() {
const { theme, toggleTheme } = useUI();
const { totalItems } = useCart();
return (
<header
style={{
padding: '15px 20px',
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
borderBottom: '2px solid ' + (theme === 'light' ? '#eee' : '#555'),
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<h1 style={{ margin: 0 }}>🛒 Shop</h1>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
<button onClick={toggleTheme}>{theme === 'light' ? '🌙' : '☀️'}</button>
<div>Cart: {totalItems} items</div>
</div>
</header>
);
}
function Filters() {
const { searchQuery, category, setSearchQuery, setCategory } = useProducts();
return (
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
<input
type='text'
placeholder='Search products...'
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ flex: 1, padding: '8px' }}
/>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
style={{ padding: '8px' }}
>
<option value='all'>All Categories</option>
<option value='electronics'>Electronics</option>
<option value='furniture'>Furniture</option>
</select>
</div>
);
}
function ProductCard({ product }) {
const { addItem } = useCart();
const { addNotification } = useUI();
const handleAddToCart = () => {
addItem(product);
addNotification(`${product.name} added to cart`, 'success');
};
return (
<div
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '15px',
textAlign: 'center',
}}
>
<h3>{product.name}</h3>
<p style={{ color: '#666', fontSize: '14px' }}>{product.category}</p>
<p style={{ fontSize: '20px', fontWeight: 'bold' }}>${product.price}</p>
<button
onClick={handleAddToCart}
style={{
padding: '8px 16px',
background: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Add to Cart
</button>
</div>
);
}
function ProductList() {
const { products, loading } = useProducts();
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
Loading products...
</div>
);
}
if (products.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
No products found
</div>
);
}
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: '15px',
}}
>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
);
}
function Cart() {
const { items, totalPrice, updateQuantity, removeItem, clearCart } =
useCart();
const { addNotification } = useUI();
if (items.length === 0) {
return (
<div
style={{
padding: '20px',
background: '#f5f5f5',
borderRadius: '8px',
textAlign: 'center',
}}
>
Your cart is empty
</div>
);
}
const handleCheckout = () => {
addNotification('Order placed successfully!', 'success');
clearCart();
};
return (
<div
style={{
padding: '20px',
background: '#f5f5f5',
borderRadius: '8px',
marginTop: '20px',
}}
>
<h2>Shopping Cart</h2>
{items.map((item) => (
<div
key={item.id}
style={{
display: 'flex',
alignItems: 'center',
padding: '10px',
background: 'white',
borderRadius: '4px',
marginBottom: '10px',
}}
>
<div style={{ flex: 1 }}>
<strong>{item.name}</strong>
<div style={{ fontSize: '14px', color: '#666' }}>
${item.price} each
</div>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
<div style={{ marginLeft: '20px', fontWeight: 'bold' }}>
${(item.price * item.quantity).toFixed(2)}
</div>
</div>
))}
<div
style={{
marginTop: '20px',
padding: '15px',
background: 'white',
borderRadius: '4px',
}}
>
<h3>Total: ${totalPrice.toFixed(2)}</h3>
<div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
<button
onClick={handleCheckout}
style={{
flex: 1,
padding: '10px',
background: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Checkout
</button>
<button
onClick={clearCart}
style={{
padding: '10px 20px',
background: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Clear
</button>
</div>
</div>
</div>
);
}
function Notifications() {
const { notifications, removeNotification } = useUI();
if (notifications.length === 0) return null;
return (
<div
style={{
position: 'fixed',
top: '20px',
right: '20px',
zIndex: 1000,
}}
>
{notifications.map((notif) => (
<div
key={notif.id}
style={{
padding: '10px 15px',
marginBottom: '10px',
background: notif.type === 'success' ? '#4caf50' : '#2196f3',
color: 'white',
borderRadius: '4px',
minWidth: '200px',
}}
>
{notif.message}
<button
onClick={() => removeNotification(notif.id)}
style={{
float: 'right',
background: 'transparent',
border: 'none',
color: 'white',
cursor: 'pointer',
}}
>
✕
</button>
</div>
))}
</div>
);
}
function App() {
const { theme } = useUI();
return (
<div
style={{
minHeight: '100vh',
background: theme === 'light' ? '#fafafa' : '#222',
color: theme === 'light' ? '#000' : '#fff',
}}
>
<Notifications />
<Header />
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
<Filters />
<ProductList />
<Cart />
</div>
</div>
);
}
function Root() {
return (
<AppProviders>
<App />
</AppProviders>
);
}
/**
* ARCHITECTURE DOCUMENTATION:
*
* CONTEXTS:
* 1. CartContext
* - Responsibility: Shopping cart state
* - State: items[], loading, error
* - Actions: ADD_ITEM, REMOVE_ITEM, UPDATE_QUANTITY, CLEAR
*
* 2. ProductContext
* - Responsibility: Product catalog & filtering
* - State: products[], searchQuery, category, loading
* - Actions: SET_PRODUCTS, SET_SEARCH_QUERY, SET_CATEGORY
*
* 3. UIContext
* - Responsibility: UI state (theme, notifications)
* - State: theme, notifications[]
* - Simple useState (không cần useReducer)
*
* USAGE EXAMPLES:
* ```jsx
* // Add to cart
* const { addItem } = useCart();
* addItem(product);
*
* // Filter products
* const { setSearchQuery, setCategory } = useProducts();
* setSearchQuery('laptop');
* setCategory('electronics');
*
* // Show notification
* const { addNotification } = useUI();
* addNotification('Success!', 'success');
* ```
*
* PERFORMANCE NOTES:
* - Context value objects recreate every render
* - Will optimize in Day 38 với useMemo
* - Current implementation prioritizes clarity over performance
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: Context + useState vs Context + useReducer
| Tiêu chí | Context + useState | Context + useReducer |
|---|---|---|
| Complexity | ✅ Đơn giản | ⚠️ Phức tạp hơn |
| State Structure | Flat, simple | Complex, nested |
| Actions | Direct setState | Action types + payloads |
| Testing | ⚠️ Test Provider | ✅ Test reducer riêng |
| Debugging | ❌ Khó trace | ✅ Action log rõ ràng |
| Code Organization | ⚠️ Logic scattered | ✅ Centralized trong reducer |
| Time Travel | ❌ Không | ✅ Có thể (undo/redo) |
| Best For | Theme, UI flags | Cart, Form, Complex state |
Bảng So Sánh: Single Context vs Multiple Contexts
| Tiêu chí | Single Context | Multiple Contexts |
|---|---|---|
| Setup | ✅ Đơn giản | ⚠️ Nhiều boilerplate |
| Performance | ❌ Mọi change → re-render all | ✅ Isolated re-renders |
| Organization | ❌ God object | ✅ Separation of concerns |
| Provider Nesting | ✅ Chỉ 1 Provider | ⚠️ Provider hell |
| Testing | ❌ Test toàn bộ | ✅ Test từng phần |
| Reusability | ❌ Khó reuse | ✅ Dễ reuse |
| Best For | Small apps | Production apps |
Decision Tree
STATE COMPLEXITY?
├─ Simple (1-2 values) → useState
└─ Complex (3+ related values) → useReducer
NUMBER OF ACTIONS?
├─ Few (1-3) → useState OK
└─ Many (5+) → useReducer
NEED TIME-TRAVEL / UNDO?
├─ Yes → useReducer (history pattern)
└─ No → useState or useReducer
MULTIPLE CONCERNS?
├─ Yes → Multiple Contexts
└─ No → Single Context OK
DATA CHANGE FREQUENCY?
├─ Often → Consider optimization (Ngày 38)
└─ Rarely → Current pattern OK🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: Dispatch trong Render ⚠️
/**
* 🐛 BUG: Maximum update depth exceeded
*
* 🎯 CHALLENGE: Tìm lỗi
*/
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
// ❌ Dispatch trong render!
if (state.count < 0) {
dispatch({ type: 'RESET' }); // Infinite loop!
}
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// ❓ QUESTIONS:
// 1. Tại sao infinite loop?
// 2. Làm sao fix?💡 Giải thích:
// NGUYÊN NHÂN:
// - dispatch() trigger re-render
// - Re-render → chạy lại component
// - if condition vẫn true → dispatch lại
// - Loop vô tận!
// ✅ FIX: Dùng useEffect
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
useEffect(() => {
if (state.count < 0) {
dispatch({ type: 'RESET' });
}
}, [state.count]);
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// RULE:
// - NEVER dispatch trong render
// - ALWAYS dispatch trong event handler hoặc useEffectBug 2: Forgot Return in Reducer ⚠️
/**
* 🐛 BUG: State không update
*
* 🎯 CHALLENGE: Tìm lỗi
*/
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
state.count + 1; // ❌ Forgot return!
case 'DECREMENT':
return { count: state.count - 1 }; // ✅ Has return
default:
return state;
}
};
// Click increment button → Không gì xảy ra!
// ❓ QUESTIONS:
// 1. Tại sao increment không hoạt động?
// 2. Decrement có hoạt động không?💡 Giải thích:
// NGUYÊN NHÂN:
// - Case INCREMENT: Tính toán nhưng KHÔNG return
// - Reducer return undefined
// - React bỏ qua undefined state
// ✅ FIX: Luôn return
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }; // ✅ Return!
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
// LƯU Ý:
// - LUÔN return trong mọi case
// - ESLint rule: "consistent-return"Bug 3: Mutating State in Reducer ⚠️
/**
* 🐛 BUG: State update không trigger re-render
*
* 🎯 CHALLENGE: Tìm lỗi
*/
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
// ❌ Mutate state directly!
state.todos.push(action.payload);
return state;
case 'TOGGLE_TODO':
// ❌ Mutate nested object!
const todo = state.todos.find((t) => t.id === action.payload);
todo.completed = !todo.completed;
return state;
default:
return state;
}
};
// Add todo → UI không update!
// ❓ QUESTIONS:
// 1. Tại sao UI không update?
// 2. Reference equality là gì?
// 3. Làm sao fix?💡 Giải thích:
// NGUYÊN NHÂN:
// - Mutate state trực tiếp
// - Return cùng reference
// - React so sánh bằng ===
// - state === state → Không re-render!
// ✅ FIX: Immutable updates
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload], // New array
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(
(
todo, // New array with new objects
) =>
todo.id === action.payload
? { ...todo, completed: !todo.completed } // New object
: todo,
),
};
default:
return state;
}
};
// RULE:
// - NEVER mutate state
// - ALWAYS return new objects/arrays
// - Use spread operator, map, filter✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu khi nào dùng Context + useState vs Context + useReducer
- [ ] Tôi biết cách kết hợp Context với useReducer
- [ ] Tôi biết cách tạo Custom Provider với helper functions
- [ ] Tôi hiểu lợi ích của encapsulating dispatch logic
- [ ] Tôi biết cách compose nhiều Contexts
- [ ] Tôi biết khi nào nên tách Contexts, khi nào nên merge
- [ ] Tôi biết cách implement undo/redo với history pattern
- [ ] Tôi hiểu immutability trong reducer
- [ ] Tôi biết KHÔNG dispatch trong render
- [ ] Tôi biết performance issues (chưa fix, chỉ aware)
Code Review Checklist
Reducer:
- [ ] Mọi case đều return
- [ ] Immutable updates (không mutate state)
- [ ] Default case return state
- [ ] Action types là constants (hoặc string rõ ràng)
Provider:
- [ ] useReducer với initial state đúng
- [ ] Helper functions encapsulate dispatch
- [ ] Computed values (derived state)
- [ ] Value object clean
Context Setup:
- [ ] Context tạo ngoài component
- [ ] Custom hook với error check
- [ ] Provider composition cho multiple contexts
Best Practices:
- [ ] Không dispatch trong render
- [ ] Action payloads có structure rõ ràng
- [ ] Validation trong reducer (nếu cần)
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Kanban Board với Context + useReducer
Tạo KanbanContext để quản lý tasks:
Requirements:
- 3 columns: Todo, In Progress, Done
- Actions: ADD_TASK, MOVE_TASK, DELETE_TASK
- Drag & drop simulation (button to move)
- Task có: id, title, description, column
State structure:
{
tasks: [{ id: 1, title: '...', description: '...', column: 'todo' }];
}Nâng cao (60 phút)
Expense Tracker với Multiple Contexts
Tạo app quản lý chi tiêu:
ExpenseContext:
- State: expenses[], categories[]
- Actions: ADD_EXPENSE, DELETE_EXPENSE, UPDATE_EXPENSE
- Computed: totalByCategory, monthlyTotal
FilterContext:
- State: dateRange, category, sortBy
- Filter expenses based on criteria
StatsContext:
- Derive stats from ExpenseContext
- Average spending, trends, etc.
Extra:
- Chart visualization (simple bars)
- Export to JSON
- Import from JSON
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Docs - useReducer:https://react.dev/reference/react/useReducer
Kent C. Dodds - Application State Management with React:https://kentcdodds.com/blog/application-state-management-with-react
Đọc thêm
React Docs - Scaling Up with Reducer and Context:https://react.dev/learn/scaling-up-with-reducer-and-context
When to useReducer vs useState:https://www.robinwieruch.de/react-usereducer-vs-usestate/
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần biết)
- Ngày 26-29: useReducer patterns (foundation cho Context + useReducer)
- Ngày 36: Context basics (Provider, useContext)
- Ngày 24: Custom hooks (wrap Context logic)
Hướng tới (sẽ dùng)
- Ngày 38: Context Performance optimization (useMemo, splitting)
- Ngày 41-43: Forms với Context + useReducer
- Ngày 45: Project 6 - Complex app architecture
💡 SENIOR INSIGHTS
Cân Nhắc Production
Khi nào dùng Context + useReducer:
// ✅ GOOD: Complex, related state
const [state, dispatch] = useReducer(cartReducer, {
items: [],
discount: 0,
shipping: 'standard',
tax: 0,
});
// ❌ BAD: Simple, unrelated states
const [theme, dispatchTheme] = useReducer(themeReducer, 'light');
// → Chỉ cần useState(theme')!Multiple Contexts Best Practice:
// ✅ GOOD: Separation of concerns
<AuthProvider> {/* Authentication */}
<CartProvider> {/* Business logic */}
<UIProvider> {/* UI state */}
<App />
</UIProvider>
</CartProvider>
</AuthProvider>
// ❌ BAD: God Context
<AppProvider> {/* Auth + Cart + UI + ... */}
<App />
</AppProvider>Helper Functions Pattern:
// ✅ GOOD: Encapsulate dispatch
const addItem = (product) => {
dispatch({ type: 'ADD_ITEM', payload: product });
};
// Components: addItem(product) ← Clean!
// ❌ BAD: Expose dispatch
// Components: dispatch({ type: 'ADD_ITEM', payload: product }) ← Messy!Câu Hỏi Phỏng Vấn
Junior:
- Context + useReducer có lợi ích gì so với Context + useState?
- Làm sao kết hợp nhiều Contexts?
- Reducer phải pure function - nghĩa là gì?
Mid:
- Implement undo/redo với useReducer như thế nào?
- Khi nào nên tách Contexts, khi nào nên merge?
- So sánh helper functions vs expose dispatch trực tiếp
Senior:
- Context + useReducer vs Redux - trade-offs?
- Làm sao optimize Context performance? (Preview Ngày 38)
- Thiết kế architecture cho large app với multiple contexts
- Middleware pattern có thể implement với useReducer không?
War Stories
Story 1: The God Context
Team dùng 1 Context cho toàn bộ app:
- Auth + Cart + Products + UI + Preferences + ...
- 1 Provider với 50+ properties
- Mọi component re-render khi bất kỳ thứ gì change
Kết quả:
- Performance tệ
- Khó debug
- Khó test
Fix:
- Tách thành 6 contexts
- Mỗi context 1 responsibility
- Performance cải thiện 80%Story 2: Forgot Immutability
Bug production:
- Cart không update UI
- Nguyên nhân: Mutate state trong reducer
- state.items.push(newItem) ← Mutate!
- React không detect change
Fix:
- Immutable updates
- [...state.items, newItem]
- Add ESLint rule🎯 PREVIEW NGÀY 38
Context Performance - The Optimization Day
Ngày mai chúng ta sẽ học:
- useMemo cho Context value
- Context splitting patterns
- Selector pattern
- Re-render optimization
- Performance profiling
Teaser:
// Hôm nay: Value object recreate mỗi render
const value = { state, dispatch }; // New object!
// Ngày mai: Memoize value
const value = useMemo(() => ({ state, dispatch }), [state]); // Same object nếu state không đổi!Chuẩn bị:
- Hiểu Context re-render behavior
- Biết object reference equality
- Suy nghĩ: Tại sao Context gây performance issues?
✅ Hoàn thành Ngày 37!
Bạn đã biết:
- Context + useReducer cho complex state
- Custom Provider pattern
- Multiple Contexts composition
- Undo/Redo implementation
- Production architecture patterns
Tiếp theo: Optimize Context performance! 🚀