State Management — Redux, Zustand, Jotai, TanStack Query
So sánh các giải pháp quản lý state trong React: Redux Toolkit, Zustand, Jotai, và TanStack Query cho server state.
Quản lý state là một trong những vấn đề khó nhất trong frontend. Server state khác client state — mỗi loại có giải pháp riêng.
Client State vs Server State#
| Client State | Server State |
|---|---|
| Theme, modal open/close | Dữ liệu từ API |
| Form input | User profile, posts |
| UI state | Cache tồn tại trên server |
| Chỉ liên quan đến UI | Cần sync với server |
Redux Toolkit — Chuẩn Mực Cho State Phức Tạp#
import { createSlice, configureStore } from '@reduxjs/toolkit';
// Slice
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [], total: 0 },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
state.total += action.payload.price;
},
removeItem: (state, action) => {
state.items = state.items.filter(i => i.id !== action.payload);
state.total = state.items.reduce((sum, i) => sum + i.price, 0);
},
},
});
export const { addItem, removeItem } = cartSlice.actions;
// Store
const store = configureStore({
reducer: {
cart: cartSlice.reducer,
},
});
// Component
function Cart() {
const items = useSelector((state) => state.cart.items);
const dispatch = useDispatch();
return (
<>
{items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => dispatch(removeItem(item.id))}>Xoá</button>
</div>
))}
</>
);
}typescriptDùng khi: State phức tạp, nhiều reducer, middleware (saga, thunk), lớn hơn vài trăm component.
Zustand — Đơn Giản, Nhẹ#
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
total: number;
addItem: (item: CartItem) => void;
removeItem: (id: number) => void;
clearCart: () => void;
}
const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
total: 0,
addItem: (item) => set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
removeItem: (id) => {
const item = get().items.find(i => i.id === id);
if (!item) return;
set((state) => ({
items: state.items.filter(i => i.id !== id),
total: state.total - item.price,
}));
},
clearCart: () => set({ items: [], total: 0 }),
}),
{ name: 'cart-storage' } // Persist to localStorage
)
);
// Component
function Cart() {
const { items, total, addItem, clearCart } = useCartStore();
return (
<div>
<p>Total: {total}</p>
<button onClick={() => addItem({ id: 1, name: 'Laptop', price: 1000 })}>
Add
</button>
<button onClick={clearCart}>Clear</button>
</div>
);
}typescriptDùng khi: State vừa, muốn đơn giản, không muốn boilerplate.
Jotai — Atomic State#
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
const userAtom = atom(async () => {
const res = await fetch('/api/user');
return res.json();
});
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
function User() {
const [user] = useAtom(userAtom);
return <div>{user.name}</div>;
}typescriptDùng khi: State phân tán, muốn atomic update, không cần store lớn.
TanStack Query (React Query) — Server State#
Giải pháp cho dữ liệu từ API — cache, refetch, stale management.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch data
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 phút mới gọi lại
});
}
// Mutate data
function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newPost) =>
fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost),
}).then(r => r.json()),
onSuccess: () => {
// Invalidate cache → tự động refetch
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
}
// Component
function Posts() {
const { data, isLoading, error } = usePosts();
const createPost = useCreatePost();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<>
<button onClick={() => createPost.mutate({ title: 'New post' })}>
Create
</button>
{data.map(post => <PostItem key={post.id} post={post} />)}
</>
);
}typescriptDùng khi: Bất kỳ dự án nào gọi API — TanStack Query nên là mặc định cho server state.
So Sánh#
| Thư viện | Client/Server | Boilerplate | Bundle size | Khi nào dùng |
|---|---|---|---|---|
| Redux Toolkit | Client | Nhiều | 11KB | App lớn, state phức tạp |
| Zustand | Client | Ít | 1KB | App vừa, đơn giản |
| Jotai | Client | Rất ít | 3KB | State phân tán, atomic |
| TanStack Query | Server | Ít | 13KB | Gần như mọi app |
Pattern Thực Tế#
// Kết hợp Zustand (UI state) + TanStack Query (server state)
// UI state
const useUIStore = create(() => ({
sidebarOpen: false,
theme: 'light',
}));
// Server state — tự động cache, refetch, optimistic update
function useProjects() {
return useQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
});
}
// Component
function Dashboard() {
const { sidebarOpen, toggleSidebar } = useUIStore();
const { data: projects, isLoading } = useProjects();
return (
<div className={sidebarOpen ? 'sidebar-open' : ''}>
<button onClick={toggleSidebar}>Toggle</button>
{isLoading ? <Spinner /> : <ProjectList projects={projects} />}
</div>
);
}typescriptKết Luận#
- Server state: TanStack Query (React Query) — mặc định cho mọi dự án
- Client state phức tạp: Redux Toolkit hoặc Zustand
- Client state đơn giản: useState/useContext là đủ
- Form state: React Hook Form (không phải state management)
Không cần Redux cho mọi project. Hầu hết app chỉ cần TanStack Query + useState + vài Zustand store nhỏ.