Trong nhiều năm, Redux cùng Redux Thunk/Saga từng là sự lựa chọn mặc định cho hầu hết các ứng dụng React. Tuy nhiên, khi các ứng dụng web ngày càng phức tạp, việc xem dữ liệu trả về từ API giống như một biến trạng thái cục bộ thông thường đã bộc lộ vô số nhược điểm: boilerplate code khổng lồ, khó đồng bộ hóa cache, và nguy cơ hiển thị dữ liệu lỗi thời (stale data).
Cách tiếp cận hiện đại tách bạch hoàn toàn bài toán State Management thành hai thế giới riêng biệt: **Server State** và **Client State**. Bài viết này sẽ phân tích chi tiết tư duy kiến trúc này và cung cấp giải pháp thực chiến với TanStack Query v5 và Zustand.
1. Bản chất khác biệt giữa Server State và Client State
Để chọn đúng công cụ, chúng ta cần hiểu rõ bản chất của dữ liệu chúng ta đang xử lý:
| Đặc điểm | Server State (Dữ liệu từ máy chủ) | Client State (Trạng thái giao diện) |
|---|---|---|
| Nơi lưu trữ gốc | Nằm trên Database/Server từ xa | Nằm trong bộ nhớ RAM trình duyệt |
| Quyền sở hữu | Không thuộc quyền kiểm soát của một user duy nhất | Hoàn toàn do user hiện tại quyết định |
| Tính đồng bộ | Bất đồng bộ (Asynchronous), có thể bị sửa bởi người khác | Đồng bộ (Synchronous), phản hồi tức thì |
| Vấn đề cốt lõi | Caching, Revalidation, Deduplication, Pagination | Bật/tắt modal, dark/light theme, step wizard |
| Công cụ tối ưu | TanStack Query (React Query), SWR, RTK Query | Zustand, Jotai, React Context |
2. Xây dựng Data Layer chuẩn với TanStack Query v5
TanStack Query không chỉ là thư viện fetch data, mà thực chất là một Async State Manager kiêm Caching Engine.
Một cấu trúc API service chuẩn mực bao gồm:
1. API Client Factory (Axios instance): Xử lý baseURL, auth tokens, interceptor refresh token.
2. Feature API Functions: Đóng gói các hàm gọi endpoint thuần túy.
3. Custom Hooks: Bọc useQuery và useMutation để component chỉ việc tiêu thụ dữ liệu mà không cần biết chi tiết HTTP.
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { studioApi, UpdateStudioPlanPayload } from "./api";
// Custom hook lấy danh sách studios có filter & pagination
export function useStudios(params: { page: number; search?: string }) {
return useQuery({
queryKey: ["studios", params.page, params.search],
queryFn: () => studioApi.getStudios(params),
staleTime: 1000 * 60 * 3, // 3 phút dữ liệu không cần fetch lại
placeholderData: (prev) => prev, // Giữ data cũ khi chuyển trang để UI không giật
});
}
// Custom hook cập nhật plan studio kèm Optimistic UI
export function useUpdateStudioPlan() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: UpdateStudioPlanPayload) => studioApi.updatePlan(payload),
onSuccess: (_, variables) => {
// Invalidate để refetch dữ liệu tươi mới từ server
queryClient.invalidateQueries({ queryKey: ["studios"] });
queryClient.invalidateQueries({ queryKey: ["studio", variables.studioId] });
},
});
}3. Quản lý Client State gọn nhẹ với Zustand
Đối với trạng thái thuần UI (như đóng mở sidebar, bộ lọc tạm thời, shopping cart offline, theme settings), Zustand vượt trội hơn hẳn so với Redux hay React Context:
- Không có Provider Wrapper: Không gây re-render toàn bộ cây component khi state thay đổi.
- Hook-based & TypeScript First: Định nghĩa store và types trong cùng một file cực kỳ trực quan.
- Hỗ trợ Middleware mạnh mẽ: Tích hợp sẵn persist (lưu vào localStorage/sessionStorage) và devtools.
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
interface FilterState {
searchQuery: string;
selectedCategory: string;
isSidebarOpen: boolean;
setSearchQuery: (query: string) => void;
setSelectedCategory: (category: string) => void;
toggleSidebar: () => void;
resetFilters: () => void;
}
export const useFilterStore = create<FilterState>()(
persist(
(set) => ({
searchQuery: "",
selectedCategory: "all",
isSidebarOpen: true,
setSearchQuery: (searchQuery) => set({ searchQuery }),
setSelectedCategory: (selectedCategory) => set({ selectedCategory }),
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
resetFilters: () => set({ searchQuery: "", selectedCategory: "all" }),
}),
{
name: "app-filters-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ selectedCategory: state.selectedCategory }), // Chỉ lưu category
}
)
);Facing similar challenges?
I can help you optimize your website — reach out for a free consultation.
4. Cách phối hợp nhịp nhàng giữa TanStack Query và Zustand
Trong thực tế, bạn sẽ lấy giá trị filter từ Zustand store để truyền vào làm queryKey của TanStack Query:
Khi người dùng nhập từ khóa tìm kiếm hoặc đổi filter, Zustand cập nhật state -> Hook useQuery tự động nhận tham số mới và kích hoạt fetch data mà không cần bất kỳ useEffect nào!
"use client";
import { useFilterStore } from "@/stores/useFilterStore";
import { useProducts } from "@/services/products/useProducts";
export function ProductCatalog() {
const { searchQuery, selectedCategory } = useFilterStore();
// Tự động re-fetch khi filter thay đổi
const { data, isLoading, error } = useProducts({
search: searchQuery,
category: selectedCategory,
});
if (isLoading) return <div>Đang tải danh sách sản phẩm...</div>;
if (error) return <div>Lỗi khi tải dữ liệu!</div>;
return (
<div className="grid grid-cols-3 gap-6">
{data?.map((product) => (
<div key={product.id} className="p-4 border rounded-xl bg-zinc-900">
<h3>{product.name}</h3>
<p className="text-emerald-400 font-bold">{product.price.toLocaleString()} đ</p>
</div>
))}
</div>
);
}Kết luận
Việc tách biệt Server State (giao cho TanStack Query) và Client State (giao cho Zustand) mang lại một codebase cực kỳ trong sáng, loại bỏ hàng trăm dòng boilerplate code rườm rà và giúp ứng dụng phản hồi mượt mà, ổn định hơn bao giờ hết.
Want to discuss further?
If you have specific questions about web performance, React, or want to collaborate on a project — reach out directly.