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 API | Redux Toolkit |
|---|---|---|
| Độ phức tạp | Đơn giản | Phức tạp hơn |
| App size | Nhỏ-vừa | Lớn |
| DevTools | Không có | ✅ Có |
| Time-travel debug | Không | ✅ Có |
| Performance | Cần tối ưu thủ công | Được tối ưu |
| Middleware | Không | ✅ Có (Thunk, Saga) |
| Ví dụ phù hợp | Theme, Auth, Language | Shopping cart, Complex state |