DDevArchive
Đăng nhập

React nâng cao: Context API, Custom Hooks & State Management

App React lớn hơn 3 component → truyền props xuyên nhiều tầng trở thành cực hình ("prop drilling"). Context API cho phép chia sẻ state không qua props. Custom hooks giúp tái sử dụng logic. Zustand cho state management đơn giản nhưng mạnh.

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
useStateState cục bộ 1 component⭐ Đơn giản nhất
Context APIState chia sẻ vài component (auth, theme)⭐⭐ Trung bình
ZustandState phức tạp, nhiều component (cart, filters)⭐⭐ Trung bình
Redux ToolkitApp 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