📅 NGÀY 36: Context API - Fundamentals
🎯 Mục tiêu học tập (5 phút)
- [ ] Hiểu vấn đề Props Drilling và tại sao cần Context
- [ ] Nắm vững cách tạo và sử dụng Context (createContext, Provider, useContext)
- [ ] Biết khi nào NÊN và KHÔNG NÊN dùng Context
- [ ] Tránh được các lỗi phổ biến khi làm việc với Context
🤔 Kiểm tra đầu vào (5 phút)
- useState và useReducer khác nhau như thế nào? Khi nào dùng useReducer?
- Custom hook là gì? Tại sao phải bắt đầu bằng "use"?
- Component re-render khi nào? State change có trigger re-render không?
📖 PHẦN 1: GIỚI THIỆU KHÁI NIỆM (30 phút)
1.1 Vấn Đề Thực Tế
Hãy tưởng tượng bạn đang xây dựng một ứng dụng e-commerce với theme switching (sáng/tối):
/**
* ❌ PROPS DRILLING PROBLEM
* Theme phải truyền qua 5 tầng component
*/
// App.jsx
function App() {
const [theme, setTheme] = useState('light');
return (
<Layout
theme={theme}
setTheme={setTheme}
/>
);
}
// Layout.jsx
function Layout({ theme, setTheme }) {
return (
<div>
<Header
theme={theme}
setTheme={setTheme}
/>
<Main theme={theme} />
</div>
);
}
// Header.jsx
function Header({ theme, setTheme }) {
return (
<Navigation
theme={theme}
setTheme={setTheme}
/>
);
}
// Navigation.jsx
function Navigation({ theme, setTheme }) {
return (
<ThemeToggle
theme={theme}
setTheme={setTheme}
/>
);
}
// ThemeToggle.jsx - Component THỰC SỰ cần dùng!
function ThemeToggle({ theme, setTheme }) {
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}Vấn đề:
- Layout, Header, Navigation KHÔNG dùng theme nhưng phải nhận props
- Khó maintain: Thêm props mới phải sửa nhiều file
- Dễ quên truyền props → bug
- Component không reusable (bị couple với props)
1.2 Giải Pháp
Context API - Cách chia sẻ data xuyên suốt component tree mà không cần props:
/**
* ✅ CONTEXT SOLUTION
* Theme có thể truy cập trực tiếp từ bất kỳ component nào
*/
// 1. Tạo Context
const ThemeContext = createContext();
// 2. App cung cấp giá trị qua Provider
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
// 3. Layout, Header, Navigation KHÔNG cần props nữa!
function Layout() {
return (
<div>
<Header />
<Main />
</div>
);
}
function Header() {
return <Navigation />;
}
function Navigation() {
return <ThemeToggle />;
}
// 4. ThemeToggle truy cập Context trực tiếp
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}Lợi ích:
- ✅ Không props drilling
- ✅ Components độc lập hơn
- ✅ Dễ thêm/sửa data
- ✅ Code cleaner
1.3 Mental Model
PROPS DRILLING:
App (theme)
→ Layout (theme)
→ Header (theme)
→ Navigation (theme)
→ ThemeToggle (theme) ← Chỉ component này dùng!
CONTEXT:
App (Provider với theme)
→ Layout
→ Header
→ Navigation
→ ThemeToggle (useContext) ← Lấy trực tiếp từ Provider!
Tương tự như: BROADCAST RADIO
- Provider = Radio Station (phát sóng)
- Context Value = Sóng radio
- useContext = Radio Receiver (bắt sóng)
→ Không cần dây dẫn (props) giữa station và receiver!1.4 Hiểu Lầm Phổ Biến
❌ "Context thay thế cho props" → ✅ Context CHỈ dùng khi data cần share nhiều nơi. Props vẫn là first choice!
❌ "Context là state management library" → ✅ Context chỉ là TRANSPORT mechanism. State vẫn dùng useState/useReducer!
❌ "Context làm app nhanh hơn" → ✅ Context có thể GÂY CHẬM nếu dùng sai (sẽ học ở Ngày 38)
❌ "Cần 1 Context duy nhất cho toàn app" → ✅ Nên tách nhiều Context cho các concerns khác nhau
💻 PHẦN 2: LIVE CODING (45 phút)
Demo 1: Pattern Cơ Bản - Theme Switcher ⭐
/**
* 🎯 Basic Context Pattern
* - createContext
* - Provider
* - useContext
*/
import { createContext, useContext, useState } from 'react';
// Step 1: Tạo Context (ngoài component!)
const ThemeContext = createContext();
// Step 2: Provider Component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
// Value object chứa state + functions
const value = {
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// Step 3: Consumer Components
function Header() {
const { theme } = useContext(ThemeContext);
return (
<header
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
}}
>
<h1>My App</h1>
<ThemeToggle />
</header>
);
}
function ThemeToggle() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'} Toggle Theme
</button>
);
}
function Content() {
const { theme } = useContext(ThemeContext);
return (
<main
style={{
background: theme === 'light' ? '#f0f0f0' : '#222',
color: theme === 'light' ? '#000' : '#fff',
minHeight: '200px',
padding: '20px',
}}
>
<p>This content adapts to theme!</p>
</main>
);
}
// Step 4: App setup
function App() {
return (
<ThemeProvider>
<Header />
<Content />
</ThemeProvider>
);
}
// Render: Header và Content tự động nhận theme, không cần props!Demo 2: Kịch Bản Thực Tế - User Authentication ⭐⭐
/**
* 🎯 Real-world Pattern: Auth Context
* - Login/Logout state
* - Loading state
* - Error handling
*/
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const login = async (email, password) => {
setLoading(true);
setError(null);
try {
// Giả lập API call
await new Promise((resolve) => setTimeout(resolve, 1000));
if (email === 'admin@test.com' && password === '123') {
setUser({ email, name: 'Admin User' });
} else {
throw new Error('Invalid credentials');
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
setError(null);
};
const value = {
user,
login,
logout,
loading,
error,
isAuthenticated: !!user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Custom hook để dùng Auth context
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Components
function LoginForm() {
const { login, loading, error } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
login(email, password);
};
return (
<form onSubmit={handleSubmit}>
<h2>Login</h2>
{error && <div style={{ color: 'red' }}>{error}</div>}
<div>
<input
type='email'
placeholder='Email'
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
/>
</div>
<div>
<input
type='password'
placeholder='Password'
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
</div>
<button
type='submit'
disabled={loading}
>
{loading ? 'Logging in...' : 'Login'}
</button>
<p style={{ fontSize: '12px', color: '#666' }}>
Hint: admin@test.com / 123
</p>
</form>
);
}
function UserProfile() {
const { user, logout } = useAuth();
return (
<div>
<h2>Welcome, {user.name}!</h2>
<p>Email: {user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
function Dashboard() {
const { isAuthenticated } = useAuth();
return <div>{isAuthenticated ? <UserProfile /> : <LoginForm />}</div>;
}
function App() {
return (
<AuthProvider>
<Dashboard />
</AuthProvider>
);
}
// Result: Login form → Enter credentials → Show user profileDemo 3: Edge Cases - Context với Default Value ⭐⭐⭐
/**
* 🎯 Edge Cases
* - Default context value
* - Missing Provider detection
* - Multiple Providers
*/
import { createContext, useContext, useState } from 'react';
// ❌ BAD: No default value
const BadContext = createContext();
// ✅ GOOD: Default value (fallback)
const LanguageContext = createContext({
language: 'en',
setLanguage: () => console.warn('No LanguageProvider found'),
});
function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en');
return (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
);
}
// Custom hook với error handling
function useLanguage() {
const context = useContext(LanguageContext);
// Edge Case 1: Component outside Provider
if (!context) {
throw new Error('useLanguage must be used within LanguageProvider');
}
return context;
}
// Edge Case 2: Multiple Providers (nested)
function NestedProvidersDemo() {
return (
<LanguageProvider>
<div>
<ComponentA />
{/* Nested Provider với giá trị khác */}
<LanguageProvider>
<ComponentB />
</LanguageProvider>
</div>
</LanguageProvider>
);
}
function ComponentA() {
const { language } = useLanguage();
return <div>Component A: {language}</div>; // 'en' from outer Provider
}
function ComponentB() {
const { language, setLanguage } = useLanguage();
// Component này dùng inner Provider
return (
<div>
Component B: {language}
<button onClick={() => setLanguage('vi')}>Change to Vietnamese</button>
</div>
);
}
// Edge Case 3: Conditional Provider
function ConditionalProviderDemo() {
const [enableProvider, setEnableProvider] = useState(false);
const content = <LanguageDisplay />;
return (
<div>
<button onClick={() => setEnableProvider(!enableProvider)}>
Toggle Provider: {enableProvider ? 'ON' : 'OFF'}
</button>
{enableProvider ? (
<LanguageProvider>{content}</LanguageProvider>
) : (
content // Dùng default value!
)}
</div>
);
}
function LanguageDisplay() {
const { language } = useContext(LanguageContext);
return (
<div>
Current Language: {language}
{/* Nếu không có Provider, sẽ dùng default value 'en' */}
</div>
);
}🔨 PHẦN 3: BÀI TẬP THỰC HÀNH (60 phút)
⭐ Level 1: Counter với Context (15 phút)
/**
* 🎯 Mục tiêu: Tạo Context đầu tiên
* ⏱️ Thời gian: 15 phút
* 🚫 KHÔNG dùng: useReducer, multiple contexts
*
* Requirements:
* 1. Tạo CounterContext với createContext
* 2. CounterProvider quản lý count state
* 3. CounterDisplay hiển thị count
* 4. CounterButtons có nút +1, -1, Reset
* 5. Tất cả components đều dùng useContext
*
* 💡 Gợi ý: Provider value nên là object { count, increment, decrement, reset }
*/
// ❌ Cách SAI:
// - Truyền count qua props thay vì Context
// - Tạo Context bên trong component (phải ở ngoài!)
// - Không wrap App bằng Provider
// - useContext() mà không có Provider
// ✅ Cách ĐÚNG: Xem solution
// 🎯 NHIỆM VỤ CỦA BẠN:
// TODO: Implement CounterContext
// TODO: Implement CounterProvider
// TODO: Implement CounterDisplay (chỉ hiển thị)
// TODO: Implement CounterButtons (các nút điều khiển)
// TODO: Kết nối trong App💡 Solution
import { createContext, useContext, useState } from 'react';
/**
* Counter Context - Basic pattern
*/
// 1. Create Context
const CounterContext = createContext();
// 2. Provider Component
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const increment = () => setCount((prev) => prev + 1);
const decrement = () => setCount((prev) => prev - 1);
const reset = () => setCount(0);
const value = {
count,
increment,
decrement,
reset,
};
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
}
// 3. Custom hook
function useCounter() {
const context = useContext(CounterContext);
if (!context) {
throw new Error('useCounter must be used within CounterProvider');
}
return context;
}
// 4. Consumer Components
function CounterDisplay() {
const { count } = useCounter();
return (
<div style={{ fontSize: '48px', textAlign: 'center', margin: '20px' }}>
{count}
</div>
);
}
function CounterButtons() {
const { increment, decrement, reset } = useCounter();
return (
<div style={{ textAlign: 'center' }}>
<button onClick={decrement}>-1</button>
<button
onClick={reset}
style={{ margin: '0 10px' }}
>
Reset
</button>
<button onClick={increment}>+1</button>
</div>
);
}
// 5. App
function App() {
return (
<CounterProvider>
<h1 style={{ textAlign: 'center' }}>Counter với Context</h1>
<CounterDisplay />
<CounterButtons />
</CounterProvider>
);
}
// Result: Counter hoạt động mượt mà, không cần props drilling⭐⭐ Level 2: Shopping Cart Context (25 phút)
/**
* 🎯 Mục tiêu: Nhận biết khi nào nên dùng Context
* ⏱️ Thời gian: 25 phút
*
* Scenario: E-commerce app cần shopping cart
* - Giỏ hàng hiển thị ở Header (số lượng items)
* - Product list ở Main
* - Checkout button ở Footer
*
* 🤔 PHÂN TÍCH:
*
* Approach A: Props Drilling
* Pros: Simple, explicit data flow
* Cons: Cart phải pass qua Header → Main → ProductCard
* Nhiều components không dùng cart phải nhận props
*
* Approach B: Context API
* Pros: Components lấy cart trực tiếp
* Dễ thêm tính năng mới
* Cons: Ẩn data flow (khó trace)
* Re-render issues nếu không optimize
*
* 💭 BẠN CHỌN GÌ VÀ TẠI SAO?
* (Document quyết định trong comment)
*
* Requirements:
* 1. CartContext quản lý items[]
* 2. addToCart(product)
* 3. removeFromCart(productId)
* 4. getTotalItems() - tổng số items
* 5. getTotalPrice() - tổng giá
*/
// 🎯 NHIỆM VỤ:
// TODO: Document approach bạn chọn (A hoặc B) và WHY
// TODO: Implement approach đã chọn
// TODO: Test với ít nhất 3 products💡 Solution
import { createContext, useContext, useState } from 'react';
/**
* DECISION: Chọn Context API (Approach B)
*
* RATIONALE:
* - Cart data cần ở nhiều nơi: Header (badge), ProductList (add button), Footer (checkout)
* - Tránh props drilling qua Layout components
* - Cart là global state, phù hợp với Context
* - Trade-off chấp nhận: Cần optimize re-render sau (Ngày 38)
*/
// 1. Create CartContext
const CartContext = createContext();
function CartProvider({ children }) {
const [items, setItems] = useState([]);
// Add item (hoặc tăng quantity nếu đã có)
const addToCart = (product) => {
setItems((prev) => {
const existingItem = prev.find((item) => item.id === product.id);
if (existingItem) {
// Tăng quantity
return prev.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item,
);
}
// Thêm mới
return [...prev, { ...product, quantity: 1 }];
});
};
const removeFromCart = (productId) => {
setItems((prev) => prev.filter((item) => item.id !== productId));
};
const getTotalItems = () => {
return items.reduce((total, item) => total + item.quantity, 0);
};
const getTotalPrice = () => {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
};
const value = {
items,
addToCart,
removeFromCart,
getTotalItems,
getTotalPrice,
};
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;
}
// 2. Components
function Header() {
const { getTotalItems } = useCart();
return (
<header style={{ borderBottom: '1px solid #ccc', padding: '10px' }}>
<h1 style={{ display: 'inline' }}>My Shop</h1>
<span style={{ float: 'right', fontSize: '20px' }}>
🛒 {getTotalItems()}
</span>
</header>
);
}
function ProductCard({ product }) {
const { addToCart } = useCart();
return (
<div style={{ border: '1px solid #ddd', padding: '10px', margin: '10px' }}>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(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 },
];
return (
<div>
<h2>Products</h2>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
);
}
function CartSummary() {
const { items, removeFromCart, getTotalPrice } = useCart();
if (items.length === 0) {
return <p>Cart is empty</p>;
}
return (
<div
style={{
borderTop: '1px solid #ccc',
padding: '10px',
marginTop: '20px',
}}
>
<h2>Cart Summary</h2>
{items.map((item) => (
<div
key={item.id}
style={{ marginBottom: '10px' }}
>
<span>
{item.name} x{item.quantity} - ${item.price * item.quantity}
</span>
<button
onClick={() => removeFromCart(item.id)}
style={{ marginLeft: '10px' }}
>
Remove
</button>
</div>
))}
<h3>Total: ${getTotalPrice()}</h3>
</div>
);
}
function App() {
return (
<CartProvider>
<Header />
<ProductList />
<CartSummary />
</CartProvider>
);
}
// Result: Header badge, ProductList, CartSummary đều access cart không cần props⭐⭐⭐ Level 3: Multi-language App (40 phút)
/**
* 🎯 Mục tiêu: Kết hợp Context với complex logic
* ⏱️ Thời gian: 40 phút
*
* 📋 Product Requirements:
* User Story: "Là user, tôi muốn đổi ngôn ngữ app (EN/VI)
* để xem nội dung bằng ngôn ngữ quen thuộc"
*
* ✅ Acceptance Criteria:
* - [ ] Language selector ở Header
* - [ ] Toàn bộ text thay đổi theo ngôn ngữ
* - [ ] Default language: 'en'
* - [ ] Hỗ trợ: English, Tiếng Việt
* - [ ] Translations object cho tất cả text
*
* 🎨 Technical Constraints:
* - Dùng Context để share language state
* - Custom hook useTranslation() để get text
* - Translations object có nested structure
*
* 🚨 Edge Cases cần handle:
* - Missing translation key → fallback to key name
* - Unknown language → fallback to 'en'
*
* 📝 Implementation Checklist:
* - [ ] LanguageContext với translations
* - [ ] LanguageProvider với state
* - [ ] useTranslation() hook
* - [ ] LanguageSelector component
* - [ ] Ít nhất 3 components dùng translations
*/💡 Solution
import { createContext, useContext, useState } from 'react';
/**
* Multi-language App với Context
* Supports: EN, VI
*/
// 1. Translations data
const translations = {
en: {
header: {
title: 'My Application',
language: 'Language',
},
home: {
welcome: 'Welcome',
description: 'This is a multi-language application demo',
currentLang: 'Current language',
},
product: {
title: 'Products',
addToCart: 'Add to Cart',
price: 'Price',
inStock: 'In Stock',
outOfStock: 'Out of Stock',
},
footer: {
copyright: '© 2024 All rights reserved',
contact: 'Contact Us',
},
},
vi: {
header: {
title: 'Ứng Dụng Của Tôi',
language: 'Ngôn ngữ',
},
home: {
welcome: 'Chào mừng',
description: 'Đây là demo ứng dụng đa ngôn ngữ',
currentLang: 'Ngôn ngữ hiện tại',
},
product: {
title: 'Sản Phẩm',
addToCart: 'Thêm vào Giỏ',
price: 'Giá',
inStock: 'Còn hàng',
outOfStock: 'Hết hàng',
},
footer: {
copyright: '© 2024 Bản quyền thuộc về',
contact: 'Liên Hệ',
},
},
};
// 2. Create Context
const LanguageContext = createContext();
function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en');
const changeLanguage = (lang) => {
if (translations[lang]) {
setLanguage(lang);
} else {
console.warn(`Language '${lang}' not supported, fallback to 'en'`);
setLanguage('en');
}
};
// Helper function để lấy nested translation
const t = (key) => {
const keys = key.split('.');
let value = translations[language];
for (const k of keys) {
if (value && value[k]) {
value = value[k];
} else {
// Fallback: return key nếu không tìm thấy
console.warn(`Translation missing: ${key} for language ${language}`);
return key;
}
}
return value;
};
const value = {
language,
changeLanguage,
t,
};
return (
<LanguageContext.Provider value={value}>
{children}
</LanguageContext.Provider>
);
}
// 3. Custom hook
function useTranslation() {
const context = useContext(LanguageContext);
if (!context) {
throw new Error('useTranslation must be used within LanguageProvider');
}
return context;
}
// 4. Components
function LanguageSelector() {
const { language, changeLanguage, t } = useTranslation();
return (
<div style={{ float: 'right' }}>
<label>{t('header.language')}: </label>
<select
value={language}
onChange={(e) => changeLanguage(e.target.value)}
>
<option value='en'>English</option>
<option value='vi'>Tiếng Việt</option>
</select>
</div>
);
}
function Header() {
const { t } = useTranslation();
return (
<header style={{ borderBottom: '2px solid #333', padding: '15px' }}>
<h1 style={{ display: 'inline' }}>{t('header.title')}</h1>
<LanguageSelector />
</header>
);
}
function Home() {
const { t, language } = useTranslation();
return (
<div style={{ padding: '20px' }}>
<h2>{t('home.welcome')}!</h2>
<p>{t('home.description')}</p>
<p>
<strong>{t('home.currentLang')}:</strong> {language.toUpperCase()}
</p>
</div>
);
}
function ProductCard({ product }) {
const { t } = useTranslation();
return (
<div
style={{
border: '1px solid #ddd',
padding: '15px',
margin: '10px',
borderRadius: '5px',
}}
>
<h3>{product.name}</h3>
<p>
<strong>{t('product.price')}:</strong> ${product.price}
</p>
<p>
{product.inStock
? `✅ ${t('product.inStock')}`
: `❌ ${t('product.outOfStock')}`}
</p>
<button>{t('product.addToCart')}</button>
</div>
);
}
function Products() {
const { t } = useTranslation();
const products = [
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Mouse', price: 29, inStock: true },
{ id: 3, name: 'Monitor', price: 399, inStock: false },
];
return (
<div style={{ padding: '20px' }}>
<h2>{t('product.title')}</h2>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
/>
))}
</div>
);
}
function Footer() {
const { t } = useTranslation();
return (
<footer
style={{
borderTop: '2px solid #333',
padding: '15px',
marginTop: '20px',
textAlign: 'center',
}}
>
<p>{t('footer.copyright')}</p>
<p>{t('footer.contact')}</p>
</footer>
);
}
function App() {
return (
<LanguageProvider>
<Header />
<Home />
<Products />
<Footer />
</LanguageProvider>
);
}
// Result: Select ngôn ngữ → Toàn bộ app thay đổi text
// Edge cases:
// - Chọn unsupported language → fallback to 'en'
// - Missing translation key → show key name + warning⭐⭐⭐⭐ Level 4: Settings Manager với Multiple Contexts (60 phút)
/**
* 🎯 Mục tiêu: Quản lý multiple contexts
* ⏱️ Thời gian: 60 phút
*
* 🏗️ PHASE 1: Research & Design (20 phút)
*
* Nhiệm vụ:
* 1. So sánh 3 approaches:
* - Single Context cho tất cả settings
* - Multiple Contexts (Theme, Language, User Preferences)
* - Nested Contexts
*
* 2. Document pros/cons mỗi approach
*
* 3. Chọn approach phù hợp nhất
*
* 4. Viết ADR (Architecture Decision Record)
*
* ADR Template:
* - Context: App cần quản lý theme, language, fontSize, notifications
* - Decision: Approach đã chọn
* - Rationale: Tại sao chọn approach này
* - Consequences: Trade-offs accepted
* - Alternatives Considered: Các options khác
*
* 💻 PHASE 2: Implementation (30 phút)
*
* Requirements:
* - ThemeContext: light/dark mode
* - LanguageContext: en/vi
* - SettingsContext: fontSize, notifications enabled/disabled
* - Settings page để control tất cả
* - Preview page hiển thị tất cả settings
*
* 🧪 PHASE 3: Testing (10 phút)
* - [ ] Change theme → UI updates
* - [ ] Change language → text updates
* - [ ] Change fontSize → text size updates
* - [ ] Toggle notifications → state updates
* - [ ] Multiple contexts không conflict
*/💡 Solution
import { createContext, useContext, useState } from 'react';
/**
* ADR: Settings Manager Architecture
*
* CONTEXT:
* App cần quản lý: theme, language, fontSize, notifications
* Mỗi setting độc lập, có thể thay đổi riêng
*
* DECISION: Multiple Contexts (Separated Concerns)
* - ThemeContext
* - LanguageContext
* - PreferencesContext (fontSize, notifications)
*
* RATIONALE:
* 1. Separation of Concerns: Mỗi context có 1 responsibility
* 2. Performance: Component chỉ re-render khi context nó dùng thay đổi
* 3. Reusability: ThemeContext có thể dùng trong app khác
* 4. Testing: Dễ test từng context riêng
*
* CONSEQUENCES (Trade-offs):
* - More boilerplate code (3 contexts thay vì 1)
* - Provider nesting (phải wrap 3 lần)
* - Phải coordinate giữa contexts nếu có dependencies
*
* ALTERNATIVES CONSIDERED:
* 1. Single Context: Đơn giản hơn, nhưng mọi change trigger re-render toàn bộ
* 2. Nested Contexts: Phức tạp, không cần thiết cho use case này
*/
// 1. 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;
}
// 2. Language Context
const LanguageContext = createContext();
const translations = {
en: {
settings: 'Settings',
theme: 'Theme',
language: 'Language',
fontSize: 'Font Size',
notifications: 'Notifications',
preview: 'Preview',
sampleText: 'This is sample text to preview settings',
},
vi: {
settings: 'Cài Đặt',
theme: 'Chủ Đề',
language: 'Ngôn Ngữ',
fontSize: 'Cỡ Chữ',
notifications: 'Thông Báo',
preview: 'Xem Trước',
sampleText: 'Đây là văn bản mẫu để xem trước cài đặt',
},
};
function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en');
const t = (key) => translations[language]?.[key] || key;
const value = { language, setLanguage, t };
return (
<LanguageContext.Provider value={value}>
{children}
</LanguageContext.Provider>
);
}
function useLanguage() {
const context = useContext(LanguageContext);
if (!context)
throw new Error('useLanguage must be used within LanguageProvider');
return context;
}
// 3. Preferences Context
const PreferencesContext = createContext();
function PreferencesProvider({ children }) {
const [fontSize, setFontSize] = useState('medium');
const [notificationsEnabled, setNotificationsEnabled] = useState(true);
const fontSizeMap = {
small: '14px',
medium: '16px',
large: '20px',
};
const value = {
fontSize,
setFontSize,
fontSizeValue: fontSizeMap[fontSize],
notificationsEnabled,
toggleNotifications: () => setNotificationsEnabled((prev) => !prev),
};
return (
<PreferencesContext.Provider value={value}>
{children}
</PreferencesContext.Provider>
);
}
function usePreferences() {
const context = useContext(PreferencesContext);
if (!context)
throw new Error('usePreferences must be used within PreferencesProvider');
return context;
}
// 4. Combined Provider (convenience)
function AppProviders({ children }) {
return (
<ThemeProvider>
<LanguageProvider>
<PreferencesProvider>{children}</PreferencesProvider>
</LanguageProvider>
</ThemeProvider>
);
}
// 5. Components
function SettingsPanel() {
const { theme, toggleTheme } = useTheme();
const { language, setLanguage, t } = useLanguage();
const { fontSize, setFontSize, notificationsEnabled, toggleNotifications } =
usePreferences();
return (
<div
style={{
padding: '20px',
background: theme === 'light' ? '#f5f5f5' : '#333',
color: theme === 'light' ? '#000' : '#fff',
borderRadius: '8px',
}}
>
<h2>{t('settings')}</h2>
{/* Theme */}
<div style={{ marginBottom: '15px' }}>
<label>{t('theme')}: </label>
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙 Dark' : '☀️ Light'}
</button>
</div>
{/* Language */}
<div style={{ marginBottom: '15px' }}>
<label>{t('language')}: </label>
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
>
<option value='en'>English</option>
<option value='vi'>Tiếng Việt</option>
</select>
</div>
{/* Font Size */}
<div style={{ marginBottom: '15px' }}>
<label>{t('fontSize')}: </label>
<select
value={fontSize}
onChange={(e) => setFontSize(e.target.value)}
>
<option value='small'>Small</option>
<option value='medium'>Medium</option>
<option value='large'>Large</option>
</select>
</div>
{/* Notifications */}
<div style={{ marginBottom: '15px' }}>
<label>
<input
type='checkbox'
checked={notificationsEnabled}
onChange={toggleNotifications}
/>{' '}
{t('notifications')}
</label>
</div>
</div>
);
}
function PreviewPanel() {
const { theme } = useTheme();
const { t } = useLanguage();
const { fontSizeValue, notificationsEnabled } = usePreferences();
return (
<div
style={{
padding: '20px',
marginTop: '20px',
background: theme === 'light' ? '#fff' : '#222',
color: theme === 'light' ? '#000' : '#fff',
borderRadius: '8px',
border: '1px solid ' + (theme === 'light' ? '#ddd' : '#555'),
}}
>
<h2>{t('preview')}</h2>
<p style={{ fontSize: fontSizeValue }}>{t('sampleText')}</p>
<div style={{ marginTop: '15px', fontSize: fontSizeValue }}>
<strong>Current Settings:</strong>
<ul>
<li>Theme: {theme}</li>
<li>Font Size: {fontSizeValue}</li>
<li>
Notifications: {notificationsEnabled ? 'Enabled' : 'Disabled'}
</li>
</ul>
</div>
</div>
);
}
function App() {
return (
<AppProviders>
<div style={{ maxWidth: '600px', margin: '20px auto' }}>
<h1 style={{ textAlign: 'center' }}>Settings Manager</h1>
<SettingsPanel />
<PreviewPanel />
</div>
</AppProviders>
);
}
/**
* TESTING CHECKLIST:
* ✅ Toggle theme → Background và text color thay đổi
* ✅ Change language → Text labels thay đổi
* ✅ Change fontSize → Preview text size thay đổi
* ✅ Toggle notifications → Checkbox state thay đổi
* ✅ Multiple contexts hoạt động độc lập
* ✅ Không có conflict giữa các contexts
*
* PERFORMANCE NOTE:
* - Khi toggle theme, chỉ components dùng useTheme re-render
* - Khi change language, chỉ components dùng useLanguage re-render
* - Optimization hơn nữa sẽ học ở Ngày 38
*/⭐⭐⭐⭐⭐ Level 5: Feature Flags System (90 phút)
/**
* 🎯 Mục tiêu: Production-ready Context pattern
* ⏱️ Thời gian: 90 phút
*
* 📋 Feature Specification:
* Feature Flags System cho phép enable/disable features trong app
* Use case: A/B testing, gradual rollout, kill switch
*
* 🏗️ Technical Design Doc:
*
* 1. Component Architecture:
* - FeatureFlagsContext: Store flags state
* - FeatureFlagsProvider: Load flags from localStorage
* - useFeatureFlag(flagName): Check if enabled
* - FeatureGate: Wrapper component để conditionally render
* - AdminPanel: UI để toggle flags
*
* 2. State Management Strategy:
* - Initial flags from localStorage (persistence)
* - Update flags → save to localStorage
* - Default flags nếu localStorage empty
*
* 3. API Integration Points:
* - (Future) Fetch flags from server
* - (Future) Real-time flag updates
*
* 4. Performance Considerations:
* - Memoize context value
* - Avoid unnecessary re-renders
*
* 5. Error Handling Strategy:
* - Invalid flag name → return false
* - localStorage error → use default flags
*
* ✅ Production Checklist:
* - [ ] Persistence với localStorage
* - [ ] Default flags configuration
* - [ ] FeatureGate component
* - [ ] useFeatureFlag hook
* - [ ] Admin panel UI
* - [ ] Error handling
* - [ ] Documentation
*
* 📝 Documentation:
* - README.md với usage examples
* - Component API documentation
*/💡 Solution
import { createContext, useContext, useState, useEffect, useMemo } from 'react';
/**
* FEATURE FLAGS SYSTEM
*
* Production-ready feature toggle system với:
* - Persistence (localStorage)
* - Type-safe flags
* - Admin UI
* - Error handling
*
* USAGE:
* ```jsx
* // Check flag
* const showNewUI = useFeatureFlag('newUI');
*
* // Conditional render
* <FeatureGate flag="newUI">
* <NewUIComponent />
* </FeatureGate>
* ```
*/
// 1. Default flags configuration
const DEFAULT_FLAGS = {
newUI: false,
darkMode: true,
analytics: false,
betaFeatures: false,
advancedSearch: false,
};
const STORAGE_KEY = 'feature_flags';
// 2. Context
const FeatureFlagsContext = createContext();
function FeatureFlagsProvider({ children }) {
// Load từ localStorage hoặc dùng defaults
const [flags, setFlags] = useState(() => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : DEFAULT_FLAGS;
} catch (error) {
console.error('Failed to load feature flags:', error);
return DEFAULT_FLAGS;
}
});
// Save to localStorage khi flags thay đổi
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(flags));
} catch (error) {
console.error('Failed to save feature flags:', error);
}
}, [flags]);
// Toggle individual flag
const toggleFlag = (flagName) => {
if (!(flagName in flags)) {
console.warn(`Unknown feature flag: ${flagName}`);
return;
}
setFlags((prev) => ({
...prev,
[flagName]: !prev[flagName],
}));
};
// Enable flag
const enableFlag = (flagName) => {
if (!(flagName in flags)) {
console.warn(`Unknown feature flag: ${flagName}`);
return;
}
setFlags((prev) => ({
...prev,
[flagName]: true,
}));
};
// Disable flag
const disableFlag = (flagName) => {
if (!(flagName in flags)) {
console.warn(`Unknown feature flag: ${flagName}`);
return;
}
setFlags((prev) => ({
...prev,
[flagName]: false,
}));
};
// Reset to defaults
const resetFlags = () => {
setFlags(DEFAULT_FLAGS);
};
// Memoize value để avoid unnecessary re-renders
const value = useMemo(
() => ({
flags,
toggleFlag,
enableFlag,
disableFlag,
resetFlags,
}),
[flags],
);
return (
<FeatureFlagsContext.Provider value={value}>
{children}
</FeatureFlagsContext.Provider>
);
}
// 3. Custom Hooks
function useFeatureFlags() {
const context = useContext(FeatureFlagsContext);
if (!context) {
throw new Error('useFeatureFlags must be used within FeatureFlagsProvider');
}
return context;
}
function useFeatureFlag(flagName) {
const { flags } = useFeatureFlags();
if (!(flagName in flags)) {
console.warn(`Unknown feature flag: ${flagName}`);
return false;
}
return flags[flagName];
}
// 4. FeatureGate Component
function FeatureGate({ flag, fallback = null, children }) {
const isEnabled = useFeatureFlag(flag);
if (!isEnabled) {
return fallback;
}
return children;
}
// 5. Admin Panel
function AdminPanel() {
const { flags, toggleFlag, resetFlags } = useFeatureFlags();
return (
<div
style={{
border: '2px solid #333',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
background: '#f9f9f9',
}}
>
<h2>🚩 Feature Flags Admin</h2>
<div style={{ marginBottom: '15px' }}>
{Object.entries(flags).map(([flagName, isEnabled]) => (
<div
key={flagName}
style={{
marginBottom: '10px',
padding: '10px',
background: '#fff',
borderRadius: '4px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div>
<strong>{flagName}</strong>
<span
style={{
marginLeft: '10px',
color: isEnabled ? 'green' : 'red',
fontWeight: 'bold',
}}
>
{isEnabled ? '✅ Enabled' : '❌ Disabled'}
</span>
</div>
<button
onClick={() => toggleFlag(flagName)}
style={{
padding: '5px 15px',
cursor: 'pointer',
background: isEnabled ? '#f44336' : '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
}}
>
{isEnabled ? 'Disable' : 'Enable'}
</button>
</div>
))}
</div>
<button
onClick={resetFlags}
style={{
padding: '10px 20px',
background: '#ff9800',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Reset to Defaults
</button>
</div>
);
}
// 6. Demo Components
function NewUI() {
return (
<div
style={{
padding: '20px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
borderRadius: '8px',
marginBottom: '15px',
}}
>
<h3>🎨 New UI Design</h3>
<p>This is the redesigned interface with modern aesthetics.</p>
</div>
);
}
function OldUI() {
return (
<div
style={{
padding: '20px',
background: '#e0e0e0',
borderRadius: '8px',
marginBottom: '15px',
}}
>
<h3>Legacy UI</h3>
<p>This is the classic interface.</p>
</div>
);
}
function AdvancedSearch() {
return (
<div
style={{
padding: '15px',
border: '2px dashed #2196F3',
borderRadius: '8px',
marginBottom: '15px',
}}
>
<h4>🔍 Advanced Search</h4>
<input
type='text'
placeholder='Search with filters...'
style={{ width: '100%', padding: '8px' }}
/>
<div style={{ marginTop: '10px' }}>
<label>
<input type='checkbox' /> Include archived
</label>
<label style={{ marginLeft: '15px' }}>
<input type='checkbox' /> Exact match
</label>
</div>
</div>
);
}
function BasicSearch() {
return (
<div style={{ marginBottom: '15px' }}>
<input
type='text'
placeholder='Basic search...'
style={{ width: '100%', padding: '8px' }}
/>
</div>
);
}
function Analytics() {
return (
<div
style={{
padding: '15px',
background: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '8px',
marginBottom: '15px',
}}
>
<h4>📊 Analytics Tracking</h4>
<p>User analytics and tracking enabled.</p>
</div>
);
}
function BetaFeatures() {
return (
<div
style={{
padding: '15px',
background: '#d1ecf1',
border: '1px solid #0c5460',
borderRadius: '8px',
marginBottom: '15px',
}}
>
<h4>🧪 Beta Features</h4>
<ul>
<li>AI-powered suggestions</li>
<li>Real-time collaboration</li>
<li>Advanced reporting</li>
</ul>
</div>
);
}
// 7. Main App
function App() {
return (
<FeatureFlagsProvider>
<div style={{ maxWidth: '800px', margin: '20px auto', padding: '20px' }}>
<h1 style={{ textAlign: 'center' }}>Feature Flags Demo</h1>
{/* Admin Panel */}
<AdminPanel />
{/* Feature-gated UI */}
<div>
<h2>Application Features</h2>
{/* New UI vs Old UI */}
<FeatureGate
flag='newUI'
fallback={<OldUI />}
>
<NewUI />
</FeatureGate>
{/* Advanced Search vs Basic Search */}
<FeatureGate
flag='advancedSearch'
fallback={<BasicSearch />}
>
<AdvancedSearch />
</FeatureGate>
{/* Optional Analytics */}
<FeatureGate flag='analytics'>
<Analytics />
</FeatureGate>
{/* Optional Beta Features */}
<FeatureGate flag='betaFeatures'>
<BetaFeatures />
</FeatureGate>
</div>
</div>
</FeatureFlagsProvider>
);
}
/**
* DOCUMENTATION:
*
* ## Installation
* Wrap app with FeatureFlagsProvider
*
* ## Usage
*
* ### Check flag in component
* ```jsx
* const showNewFeature = useFeatureFlag('newFeature');
* if (showNewFeature) {
* return <NewFeature />;
* }
* ```
*
* ### Conditional rendering
* ```jsx
* <FeatureGate flag="newFeature" fallback={<OldFeature />}>
* <NewFeature />
* </FeatureGate>
* ```
*
* ### Programmatic control
* ```jsx
* const { enableFlag, disableFlag } = useFeatureFlags();
* enableFlag('newFeature');
* ```
*
* ## Error Handling
* - Unknown flags return false + warning
* - localStorage errors fallback to defaults
*
* ## Performance
* - Context value is memoized
* - Only re-renders when flags change
* - Persist to localStorage automatically
*/📊 PHẦN 4: SO SÁNH PATTERNS (30 phút)
Bảng So Sánh: Props vs Context
| Tiêu chí | Props Drilling | Context API |
|---|---|---|
| Use Case | Data qua 1-2 level | Data qua nhiều levels (3+) |
| Explicit | ✅ Rõ ràng data flow | ❌ Ẩn, khó trace |
| Reusability | ✅ Component độc lập | ⚠️ Phụ thuộc Provider |
| Performance | ✅ Optimal | ⚠️ Re-render issues (nếu không optimize) |
| Refactoring | ❌ Khó (phải sửa nhiều files) | ✅ Dễ (chỉ sửa Provider) |
| Testing | ✅ Dễ test (pass props) | ⚠️ Cần mock Provider |
| Debugging | ✅ Dễ debug | ⚠️ Khó trace source |
| Best For | Component composition | Global/cross-cutting concerns |
Bảng So Sánh: Context vs External State (Preview)
| Tiêu chí | Context API | Redux/Zustand (chưa học) |
|---|---|---|
| Learning Curve | ✅ Đơn giản | ⚠️ Phức tạp |
| Boilerplate | ✅ Ít code | ❌ Nhiều code |
| DevTools | ❌ Không có | ✅ Redux DevTools |
| Performance | ⚠️ Manual optimization | ✅ Auto-optimized |
| When to use | Simple state sharing | Complex state, time-travel |
Decision Tree
CẦN SHARE STATE?
├─ NO → Dùng props hoặc local state
└─ YES → Data qua bao nhiêu levels?
├─ 1-2 levels → Props drilling (OK)
└─ 3+ levels → Có phải global concern?
├─ YES (theme, auth, language) → Context
└─ NO (business logic) → Xem xét Redux (học sau)
DATA THAY ĐỔI THƯỜNG XUYÊN?
├─ Ít (theme, config) → Context OK
└─ Nhiều (real-time data) → Xem xét external state
SỐ CONSUMERS?
├─ Ít (2-3 components) → Props hoặc Context
└─ Nhiều (10+ components) → Context + Optimization (Ngày 38)🧪 PHẦN 5: DEBUG LAB (20 phút)
Bug 1: useContext Outside Provider ⚠️
/**
* 🐛 BUG: Component crash với error
* "Cannot read property 'theme' of undefined"
*
* 🎯 CHALLENGE: Tìm lỗi và fix
*/
const ThemeContext = createContext();
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext); // ❌ Error!
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle
</button>
);
}
function App() {
return <ThemeToggle />; // ❌ Không có Provider!
}
// ❓ QUESTIONS:
// 1. Tại sao lỗi?
// 2. Làm sao fix?
// 3. Làm sao prevent lỗi này?💡 Giải thích:
// NGUYÊN NHÂN:
// - useContext(ThemeContext) trả về undefined
// - Vì không có Provider wrap component
// - Destructuring undefined → crash
// ✅ FIX 1: Wrap với Provider
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<ThemeToggle />
</ThemeContext.Provider>
);
}
// ✅ FIX 2: Custom hook với error check
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// PREVENTION:
// - LUÔN tạo custom hook với error check
// - Setup ESLint rule để detect missing ProviderBug 2: Stale Value trong Context ⚠️
/**
* 🐛 BUG: Callback trong Context luôn dùng giá trị cũ
*
* 🎯 CHALLENGE: Tìm lỗi và fix
*/
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
// ❌ Function này tạo closure với count ban đầu (0)
const increment = () => {
setCount(count + 1); // count luôn là 0!
};
const value = { count, increment };
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
}
function Counter() {
const { count, increment } = useContext(CounterContext);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+1</button>
{/* Click nhiều lần nhưng count chỉ lên 1! */}
</div>
);
}
// ❓ QUESTIONS:
// 1. Tại sao count chỉ lên 1?
// 2. Stale closure là gì?
// 3. Làm sao fix?💡 Giải thích:
// NGUYÊN NHÂN:
// - increment function tạo closure với count = 0
// - Mỗi lần Provider re-render, function mới được tạo
// - Nhưng function vẫn reference count cũ
// ✅ FIX: Dùng functional update
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
// Functional update không phụ thuộc vào count hiện tại
const increment = () => {
setCount((prev) => prev + 1); // ✅ Luôn đúng!
};
const value = { count, increment };
return (
<CounterContext.Provider value={value}>{children}</CounterContext.Provider>
);
}
// LƯU Ý:
// - LUÔN dùng functional update trong callbacks
// - Đặc biệt khi callback được memoize (useCallback - Ngày 34)Bug 3: Context Value Object Recreation ⚠️
/**
* 🐛 BUG: Component re-render không cần thiết
*
* 🎯 CHALLENGE: Tìm performance issue
*/
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// ❌ Mỗi render tạo object mới!
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ExpensiveComponent() {
const { theme } = useContext(ThemeContext);
console.log('ExpensiveComponent rendered!');
// Component này render MỖI KHI ThemeProvider render
// Dù theme không đổi!
return <div>Theme: {theme}</div>;
}
// ❓ QUESTIONS:
// 1. Tại sao ExpensiveComponent re-render nhiều?
// 2. Object identity là gì?
// 3. Làm sao optimize?💡 Giải thích:
// NGUYÊN NHÂN:
// - { theme, setTheme } tạo object MỚI mỗi render
// - Context so sánh value bằng reference (===)
// - Object mới → Context thay đổi → consumers re-render
// ✅ FIX: Sẽ học ở Ngày 38 (useMemo)
// (Tạm thời chấp nhận bug này, không optimize sớm)
// PREVIEW (Ngày 38):
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Memoize value object
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
// LƯU Ý:
// - KHÔNG optimize sớm!
// - Chỉ optimize khi có performance issue thật
// - Measure first, optimize later✅ PHẦN 6: TỰ ĐÁNH GIÁ (15 phút)
Knowledge Check
- [ ] Tôi hiểu Props Drilling là gì và tại sao nó là vấn đề
- [ ] Tôi biết cách tạo Context với createContext()
- [ ] Tôi biết cách cung cấp value qua Provider
- [ ] Tôi biết cách consume value với useContext()
- [ ] Tôi biết khi nào NÊN dùng Context
- [ ] Tôi biết khi nào KHÔNG NÊN dùng Context (props đơn giản hơn)
- [ ] Tôi biết cách tạo custom hook cho Context
- [ ] Tôi biết cách handle missing Provider
- [ ] Tôi hiểu stale closure trong Context callbacks
- [ ] Tôi biết Context value object recreation issue (chưa fix, chỉ biết)
Code Review Checklist
Context Setup:
- [ ] Context tạo NGOÀI component (không tạo lại mỗi render)
- [ ] Provider có default value (fallback)
- [ ] Custom hook có error check cho missing Provider
Value Object:
- [ ] Value object chứa cả state và functions
- [ ] Functions dùng functional updates (tránh stale closure)
- [ ] (Chưa optimize) Biết rằng object tạo mới mỗi render
Usage:
- [ ] Chỉ dùng Context khi data qua 3+ levels
- [ ] Props drilling cho 1-2 levels
- [ ] Context cho global concerns (theme, auth, language)
- [ ] KHÔNG dùng Context cho business logic (chưa phức tạp)
Error Handling:
- [ ] Custom hook throw error nếu outside Provider
- [ ] Default value cho Context (fallback)
🏠 BÀI TẬP VỀ NHÀ
Bắt buộc (30 phút)
Notification System với Context
Tạo NotificationContext để quản lý toast notifications:
Requirements:
- addNotification(message, type) - type: 'success', 'error', 'info'
- removeNotification(id)
- Notifications tự động biến mất sau 3 giây
- Tối đa 3 notifications cùng lúc
- Position: top-right của màn hình
Gợi ý:
const notification = {
id: Date.now(),
message: 'Success!',
type: 'success',
};Nâng cao (60 phút)
Modal Manager với Context
Tạo ModalContext để quản lý modals:
Requirements:
- openModal(component, props)
- closeModal()
- Chỉ 1 modal active tại 1 thời điểm
- Click overlay để đóng
- ESC key để đóng
- Prevent body scroll khi modal mở
Extra:
- Multiple modals stack (array)
- Modal history (back/forward)
📚 TÀI LIỆU THAM KHẢO
Bắt buộc đọc
React Docs - Context:https://react.dev/learn/passing-data-deeply-with-context
Kent C. Dodds - How to use React Context effectively:https://kentcdodds.com/blog/how-to-use-react-context-effectively
Đọc thêm
React Docs - useContext:https://react.dev/reference/react/useContext
When (and when not) to use Context:https://blog.logrocket.com/react-context-api-deep-dive-examples/
🔗 KẾT NỐI KIẾN THỨC
Kiến thức nền (cần biết)
- Ngày 11-12: useState patterns (Context dùng state internally)
- Ngày 21-22: useRef (để so sánh với Context use cases)
- Ngày 24: Custom hooks (Context thường wrap trong custom hooks)
Hướng tới (sẽ dùng)
- Ngày 37: Context + useReducer (powerful combo!)
- Ngày 38: Context performance optimization (useMemo)
- Ngày 41-43: Form context với React Hook Form
💡 SENIOR INSIGHTS
Cân Nhắc Production
Context KHÔNG phải State Management:
// ❌ Anti-pattern: Dùng Context như Redux
const AppContext = createContext();
function AppProvider({ children }) {
const [users, setUsers] = useState([]);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
// ... 20 states nữa
// ❌ Quá nhiều responsibility!
}
// ✅ Better: Tách thành nhiều contexts
<UserProvider>
<PostProvider>
<CommentProvider>
<App />
</CommentProvider>
</PostProvider>
</UserProvider>;Context cho Configuration, không phải Cache:
// ✅ GOOD: Theme, language (ít thay đổi)
<ThemeProvider>
<LanguageProvider>
// ❌ BAD: API data (thay đổi liên tục)
<APIDataProvider>
// → Dùng React Query (học sau)Multiple Providers Pattern:
// ✅ Composition pattern
function AppProviders({ children }) {
return (
<ThemeProvider>
<LanguageProvider>
<AuthProvider>
<NotificationProvider>{children}</NotificationProvider>
</AuthProvider>
</LanguageProvider>
</ThemeProvider>
);
}Câu Hỏi Phỏng Vấn
Junior:
- Context API là gì? Giải quyết vấn đề gì?
- Props drilling là gì? Tại sao là vấn đề?
- useContext hook dùng như thế nào?
Mid:
- So sánh Props drilling vs Context. Khi nào dùng cái nào?
- Làm sao handle missing Provider?
- Tại sao nên tạo custom hook cho Context?
Senior:
- Context có performance issues gì? Làm sao optimize?
- So sánh Context API vs Redux/Zustand
- Khi nào KHÔNG nên dùng Context?
- Multiple contexts vs single context - trade-offs?
War Stories
Story 1: The Context Overuse
Team dùng Context cho MỌI THỨ:
- User data → Context
- API responses → Context
- Form state → Context
- UI state → Context
Kết quả:
- Re-render nightmare
- Khó debug
- Performance tệ
Fix:
- Chỉ dùng Context cho global concerns
- Local state cho local data
- React Query cho server stateStory 2: Missing Provider Hell
Component crash production:
"Cannot read property 'user' of undefined"
Nguyên nhân:
- useContext(AuthContext) nhưng thiếu Provider
- Không có error boundary
- Không có default value
Fix:
- Custom hook với error check
- Default value cho Context
- Error boundaries wrap app🎯 PREVIEW NGÀY 37
Context + useReducer - The Power Combo
Ngày mai chúng ta sẽ học:
- Kết hợp Context với useReducer
- Patterns cho complex state
- Dispatch actions qua Context
- State machine với Context
Teaser:
// Context + useReducer = 🔥
const [state, dispatch] = useReducer(reducer, initialState);
<AppContext.Provider value={{ state, dispatch }}>
{/* Mọi component có thể dispatch actions! */}
</AppContext.Provider>;Chuẩn bị:
- Review useReducer (Ngày 26-29)
- Hiểu Context patterns (Ngày 36)
- Suy nghĩ: Làm sao kết hợp 2 concepts này?
✅ Hoàn thành Ngày 36!
Bạn đã biết:
- Props Drilling và Context API
- createContext, Provider, useContext
- Custom hooks cho Context
- Common pitfalls và cách tránh
Tiếp theo: Context + useReducer cho complex state management! 🚀