Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Context API

Khái niệm

Context API giải quyết vấn đề prop drilling - truyền props qua nhiều cấp component không cần thiết.

Không có Context (Prop Drilling):
App → Page → Section → Panel → Button (button cần theme)
     (pass)   (pass)   (pass)   (dùng)

Với Context:
App (Provider theme) ──────────────────→ Button (useContext)
     ↓              ↓               ↓
    Page          Section          Panel
   (không biết gì về theme)

createContext & useContext

import { createContext, useContext, useState, useMemo } from 'react';

// 1. Tạo context với default value
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

// 2. Tạo Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  };

  // Memoize value để tránh re-render không cần thiết
  const value = useMemo(
    () => ({ theme, toggleTheme }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Custom hook để sử dụng context (best practice)
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 4. Sử dụng
function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header className={`header header--${theme}`}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} mode
      </button>
    </header>
  );
}

function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}

Auth Context Pattern

Ví dụ thực tế: quản lý authentication state.

import { createContext, useContext, useState, useCallback } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
  roles: string[];
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = useCallback(async (email: string, password: string) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) throw new Error('Login failed');

    const data = await response.json();
    setUser(data.user);
    localStorage.setItem('token', data.token);
  }, []);

  const logout = useCallback(() => {
    setUser(null);
    localStorage.removeItem('token');
  }, []);

  const value: AuthContextType = {
    user,
    isAuthenticated: user !== null,
    login,
    logout,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Sử dụng
function NavBar() {
  const { user, isAuthenticated, logout } = useAuth();

  return (
    <nav>
      {isAuthenticated ? (
        <>
          <span>Welcome, {user?.name}</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <a href="/login">Login</a>
      )}
    </nav>
  );
}

Multiple Contexts

// Tách contexts theo chức năng
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          <Router>
            <AppContent />
          </Router>
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// Hoặc combine thành một AppProvider
function AppProvider({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          {children}
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

Context Performance - Tránh Re-renders không cần thiết

// ❌ Vấn đề: Mọi consumer re-render khi bất kỳ value nào thay đổi
const AppContext = createContext(null);

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [cart, setCart] = useState([]);

  // Object mới mỗi render → mọi consumer re-render!
  return (
    <AppContext.Provider value={{ user, theme, cart, setUser, setTheme, setCart }}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Giải pháp 1: Tách contexts
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const CartContext = createContext(null);

// ✅ Giải pháp 2: useMemo
function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        {children}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

Khi nào dùng Context vs Redux

Tiêu chíContext APIRedux Toolkit
Độ phức tạpĐơn giảnPhức tạp hơn
App sizeNhỏ-vừaLớn
DevToolsKhông có✅ Có
Time-travel debugKhông✅ Có
PerformanceCần tối ưu thủ côngĐược tối ưu
MiddlewareKhông✅ Có (Thunk, Saga)
Ví dụ phù hợpTheme, Auth, LanguageShopping cart, Complex state