Prop drilling: vấn đề phải giải quyết
Khi component A cần truyền data cho component D (qua B, C), B và C phải nhận prop “hộ” dù không dùng. Đây là prop drilling — code xấu, khó bảo trì.
// File: context.tsx
import { createContext, useContext, useState, ReactNode } from 'react'
// 1. Tạo context
interface AuthContext {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
}
const AuthCtx = createContext<AuthContext | null>(null)
// 2. Provider: bọc app, cung cấp state
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
async function login(email: string, password: string) {
const res = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
})
const data = await res.json()
setUser(data.user)
}
function logout() { setUser(null) }
return (
<AuthCtx.Provider value={{ user, login, logout }}>
{children}
</AuthCtx.Provider>
)
}
// 3. Custom hook: dùng context dễ dàng
function useAuth() {
const ctx = useContext(AuthCtx)
if (!ctx) throw new Error("useAuth phải dùng trong AuthProvider")
return ctx
}
// 4. Dùng ở BẤT KỲ component nào — không cần truyền props
function Navbar() {
const { user, logout } = useAuth()
return user ? <button onClick={logout}>Đăng xuất</button> : null
}
Custom Hooks: tái sử dụng logic
// File: hooks.ts
// Custom hook: fetch data từ API
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const controller = new AbortController()
async function fetchData() {
try {
setLoading(true)
const res = await fetch(url, { signal: controller.signal })
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
setData(json)
} catch (err) {
if (err instanceof Error && err.name !== "AbortError") {
setError(err.message)
}
} finally {
setLoading(false)
}
}
void fetchData()
return () => controller.abort() // cleanup khi unmount
}, [url])
return { data, loading, error }
}
// Dùng: 1 dòng thay vì 20 dòng lặp lại
function UserList() {
const { data: users, loading, error } = useFetch<User[]>("/api/users")
if (loading) return <p>Đang tải...</p>
if (error) return <p>Lỗi: {error}</p>
return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
Zustand: state management đơn giản
Redux quá phức tạp cho hầu hết app. Zustand cung cấp global state với API cực đơn giản — không boilerplate, không provider, không action types.
// File: store.ts
import { create } from 'zustand'
// Tạo store trong 10 dòng
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
total: () => number
}
const useCart = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
removeItem: (id) => set((s) => ({
items: s.items.filter((i) => i.id !== id),
})),
total: () => get().items.reduce((sum, i) => sum + i.price, 0),
}))
// Dùng ở bất kỳ component nào — không cần Provider
function CartButton() {
const items = useCart((s) => s.items)
return <span>Giỏ hàng ({items.length})</span>
}
| Công cụ | Khi nào dùng | Độ phức tạp |
|---|---|---|
| useState | State cục bộ 1 component | ⭐ Đơn giản nhất |
| Context API | State chia sẻ vài component (auth, theme) | ⭐⭐ Trung bình |
| Zustand | State phức tạp, nhiều component (cart, filters) | ⭐⭐ Trung bình |
| Redux Toolkit | App rất lớn, team đông, cần middleware | ⭐⭐⭐ Phức tạp |
❓ Khi nào nên dùng Context API thay vì useState?
- Tạo Context + Provider + custom hook
- Viết custom hook useFetch với TypeScript
- Tạo Zustand store cơ bản
- Biết khi nào dùng useState vs Context vs Zustand