State management React đã thay đổi nhiều từ "Redux mọi case" sang lựa chọn theo use case. Bài này so sánh Zustand, Redux Toolkit, Context — kèm code thực tế và quy tắc khi nào dùng cái nào.
3 cấp độ state
- Local: useState/useReducer trong component. Form input, modal open, hover state.
- Shared: Context API hoặc state library. Theme, user info, cart, filter.
- Server: TanStack Query / SWR / RTK Query. API data có cache, refetch, sync.
Đừng nhầm 2 và 3. Nhồi server data vào Redux → reinvent cache + duplicate state. TanStack Query handle tốt hơn nhiều.
useState — đa số case đủ
function CommentForm() {
const [text, setText] = useState('')
const [submitting, setSubmitting] = useState(false)
// ... 90% form chỉ cần useState
}
Đừng over-engineer. Form 1 page, modal toggle, dropdown — useState là đủ.
Context API — config app-level
// theme-context.tsx
const ThemeContext = createContext<{ theme: 'light' | 'dark', toggle: () => void }>(null!)
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')
return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>
}
export const useTheme = () => useContext(ThemeContext)
Pitfall: mọi consumer re-render khi value đổi. Cart có 100 item, mỗi lần thêm → 100 component re-render. Context tốt cho data ÍT đổi (theme, locale, current user).
Zustand — default 2026
npm install zustand
// stores/cart.ts
import { create } from 'zustand'
interface CartState {
items: { id: number, qty: number }[]
addItem: (id: number) => void
removeItem: (id: number) => void
total: number
}
export const useCart = create<CartState>((set, get) => ({
items: [],
addItem: (id) => set(state => ({
items: state.items.find(i => i.id === id)
? state.items.map(i => i.id === id ? { ...i, qty: i.qty + 1 } : i)
: [...state.items, { id, qty: 1 }],
})),
removeItem: (id) => set(state => ({
items: state.items.filter(i => i.id !== id)
})),
get total() { return get().items.reduce((s, i) => s + i.qty, 0) },
}))
// Component: selective re-render qua selector
function Cart() {
// Chỉ re-render khi total đổi, không khi items toàn bộ đổi
const total = useCart(state => state.total)
const addItem = useCart(state => state.addItem)
return <button onClick={() => addItem(42)}>Cart ({total})</button>
}
Selective re-render là điểm mạnh chính của Zustand vs Context.
Persistent store qua middleware:
import { persist } from 'zustand/middleware'
export const useCart = create(persist(
(set, get) => ({ /* ... */ }),
{ name: 'cart', storage: createJSONStorage(() => localStorage) }
))
Redux Toolkit — app lớn complex
// store/cart.ts
import { createSlice, configureStore } from '@reduxjs/toolkit'
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] as { id: number, qty: number }[] },
reducers: {
addItem: (state, action) => {
const existing = state.items.find(i => i.id === action.payload)
if (existing) existing.qty++
else state.items.push({ id: action.payload, qty: 1 })
},
removeItem: (state, action) => {
state.items = state.items.filter(i => i.id !== action.payload)
},
},
})
export const { addItem, removeItem } = cartSlice.actions
export const store = configureStore({
reducer: { cart: cartSlice.reducer },
})
export type RootState = ReturnType<typeof store.getState>
// Component
import { useSelector, useDispatch } from 'react-redux'
function Cart() {
const total = useSelector((s: RootState) =>
s.cart.items.reduce((sum, i) => sum + i.qty, 0)
)
const dispatch = useDispatch()
return <button onClick={() => dispatch(addItem(42))}>Cart ({total})</button>
}
RTK đã ít boilerplate hơn Redux cũ rất nhiều. Vẫn verbose hơn Zustand 2-3x. Trade-off: middleware ecosystem (logger, saga, RTK Query), Redux DevTools time-travel.
Server state — TanStack Query
import { useQuery } from '@tanstack/react-query'
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
staleTime: 60_000,
})
if (isLoading) return <Spinner />
if (error) return <Error />
return <div>{data.name}</div>
}
TanStack Query handle: cache, dedupe, refetch on focus, optimistic update, infinite scroll. Đừng tự viết.
Khung quyết định
| Use case | Tool |
|---|---|
| Local form, toggle, hover | useState |
| Theme, locale, current user (ít đổi) | Context |
| Client state share, đổi nhiều | Zustand |
| App lớn cần middleware, time-travel | Redux Toolkit |
| Server data (API) | TanStack Query / SWR |
| State phụ thuộc phức tạp (atom graph) | Jotai |
Kết luận
State management 2026: bắt đầu với useState, dùng Context cho config app, Zustand cho client state share, TanStack Query cho server data. Redux chỉ khi đã invest hoặc có middleware phức tạp. Tham khảo Form handling để biết khi nào dùng react-hook-form thay state library cho form.