Skip to content

📅 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)

  1. useState và useReducer khác nhau như thế nào? Khi nào dùng useReducer?
  2. Custom hook là gì? Tại sao phải bắt đầu bằng "use"?
  3. 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):

jsx
/**
 * ❌ 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:

jsx
/**
 * ✅ 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 ⭐

jsx
/**
 * 🎯 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 ⭐⭐

jsx
/**
 * 🎯 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 profile

Demo 3: Edge Cases - Context với Default Value ⭐⭐⭐

jsx
/**
 * 🎯 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)

jsx
/**
 * 🎯 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
jsx
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)

jsx
/**
 * 🎯 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
jsx
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)

jsx
/**
 * 🎯 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
jsx
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)

jsx
/**
 * 🎯 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
jsx
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)

jsx
/**
 * 🎯 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
jsx
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 DrillingContext API
Use CaseData qua 1-2 levelData 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 ForComponent compositionGlobal/cross-cutting concerns

Bảng So Sánh: Context vs External State (Preview)

Tiêu chíContext APIRedux/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 useSimple state sharingComplex 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 ⚠️

jsx
/**
 * 🐛 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:

jsx
// 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 Provider

Bug 2: Stale Value trong Context ⚠️

jsx
/**
 * 🐛 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:

jsx
// 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 ⚠️

jsx
/**
 * 🐛 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:

jsx
// 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 ý:

jsx
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

  1. React Docs - Context:https://react.dev/learn/passing-data-deeply-with-context

  2. Kent C. Dodds - How to use React Context effectively:https://kentcdodds.com/blog/how-to-use-react-context-effectively

Đọc thêm

  1. React Docs - useContext:https://react.dev/reference/react/useContext

  2. 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:

jsx
// ❌ 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:

jsx
// ✅ 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:

jsx
// ✅ Composition pattern
function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <LanguageProvider>
        <AuthProvider>
          <NotificationProvider>{children}</NotificationProvider>
        </AuthProvider>
      </LanguageProvider>
    </ThemeProvider>
  );
}

Câu Hỏi Phỏng Vấn

Junior:

  1. Context API là gì? Giải quyết vấn đề gì?
  2. Props drilling là gì? Tại sao là vấn đề?
  3. useContext hook dùng như thế nào?

Mid:

  1. So sánh Props drilling vs Context. Khi nào dùng cái nào?
  2. Làm sao handle missing Provider?
  3. Tại sao nên tạo custom hook cho Context?

Senior:

  1. Context có performance issues gì? Làm sao optimize?
  2. So sánh Context API vs Redux/Zustand
  3. Khi nào KHÔNG nên dùng Context?
  4. 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 state

Story 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:

jsx
// 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! 🚀

Personal tech knowledge base