Skip to main content
Architecture12 min read27/08/2026

Kiến trúc State Management chuẩn Enterprise: Tách biệt triệt để Server State & Client State

Vì sao không nên nhồi nhét mọi thứ vào Redux? Hướng dẫn thiết lập kiến trúc State phân tầng: TanStack Query v5 xử lý API cache & mutation kết hợp Zustand quản lý UI state siêu gọn nhẹ.

N

Nguyễn Duy Noa

Frontend Developer

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ểmServer State (Dữ liệu từ máy chủ)Client State (Trạng thái giao diện)
Nơi lưu trữ gốcNằm trên Database/Server từ xaNằm trong bộ nhớ RAM trình duyệt
Quyền sở hữuKhông thuộc quyền kiểm soát của một user duy nhấtHoà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õiCaching, Revalidation, Deduplication, PaginationBật/tắt modal, dark/light theme, step wizard
Công cụ tối ưuTanStack Query (React Query), SWR, RTK QueryZustand, 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 useQueryuseMutation để component chỉ việc tiêu thụ dữ liệu mà không cần biết chi tiết HTTP.

services/studios/useStudios.ts
typescript
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.

stores/useFilterStore.ts
typescript
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!

components/ProductCatalog.tsx
tsx
"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.

State ManagementTanStack QueryZustandReactTypeScriptClean Architecture

Want to discuss further?

If you have specific questions about web performance, React, or want to collaborate on a project — reach out directly.

Send an email