Trong hệ sinh thái React, quản lý trạng thái toàn cục (Global State Management) luôn là đề tài gây nhiều tranh luận. Redux Toolkit tuy mạnh mẽ nhưng lại quá cồng kềnh với nhiều boilerplate code; React Context API thì đơn giản nhưng lại dễ gây ra vấn đề hiệu năng (Re-render toàn bộ cây component con không cần thiết).
Zustand ra đời như một vị cứu tinh:
- Siêu nhẹ (Dung lượng < 1KB): Không làm nặng bundle của ứng dụng.
- Không cần Provider bọc ngoài: Có thể đọc và cập nhật state ở bất kỳ đâu (kể cả bên ngoài React component).
- Atomic Selector: Chỉ component nào thực sự lắng nghe dữ liệu bị thay đổi mới bị re-render.
- Tích hợp sẵn Middleware mạnh mẽ: Hỗ trợ lưu trữ bền vững (
persistvào LocalStorage/SessionStorage), gỡ lỗi qua Redux DevTools, và Immer.
Bài viết này sẽ hướng dẫn bạn từ cách cài đặt, thiết lập Store chuẩn TypeScript cho tới việc khắc phục triệt để lỗi Hydration Mismatch trong Next.js App Router.
1. Cài đặt Zustand
Cài đặt thư viện Zustand bằng package manager:
# Dùng pnpm
pnpm add zustand
# Hoặc dùng npm / yarn / bun
# npm install zustand
# yarn add zustand2. Khởi tạo Store cơ bản chuẩn TypeScript
Tạo một store quản lý giao diện người dùng (UI Store) như trạng thái bật/tắt Sidebar, Dark Mode, hoặc Modal thông báo:
"use client";
import { create } from "zustand";
interface UiState {
isSidebarOpen: boolean;
theme: "dark" | "light";
activeModal: string | null;
toggleSidebar: () => void;
setTheme: (theme: "dark" | "light") => void;
openModal: (modalId: string) => void;
closeModal: () => void;
}
export const useUiStore = create<UiState>((set) => ({
isSidebarOpen: false,
theme: "dark",
activeModal: null,
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
setTheme: (theme) => set({ theme }),
openModal: (modalId) => set({ activeModal: modalId }),
closeModal: () => set({ activeModal: null }),
}));3. Sử dụng trong Component & Kỹ thuật chặn Re-render thừa
Một trong những ưu điểm lớn nhất của Zustand là Atomic Selector. Bạn chỉ nên trích xuất đúng phần dữ liệu mà component đó cần:
"use client";
import { useUiStore } from "@/src/store/useUiStore";
export function Header() {
// ✅ ĐÚNG: Chỉ lắng nghe isSidebarOpen và toggleSidebar
// Khi theme thay đổi, Header này HOÀN TOÀN KHÔNG BỊ RE-RENDER!
const isSidebarOpen = useUiStore((state) => state.isSidebarOpen);
const toggleSidebar = useUiStore((state) => state.toggleSidebar);
return (
<header className="flex items-center justify-between p-4 bg-zinc-900 border-b border-zinc-800">
<h1 className="text-lg font-bold text-zinc-100">My App</h1>
<button
onClick={toggleSidebar}
className="px-3 py-1.5 text-xs font-semibold rounded-lg bg-zinc-800 text-zinc-200"
>
{isSidebarOpen ? "Đóng Menu" : "Mở Menu"}
</button>
</header>
);
}Facing similar challenges?
I can help you optimize your website — reach out for a free consultation.
4. Khắc phục lỗi Hydration Mismatch khi dùng Middleware Persist
Khi sử dụng middleware persist để lưu state xuống localStorage, trên Server component state luôn mang giá trị khởi tạo mặc định, trong khi trên Client browser state đã được đọc từ localStorage. Sự sai lệch này gây ra lỗi Text content does not match server-rendered HTML (Hydration Mismatch).
Giải pháp: Viết một hook useHydratedStore an toàn cho SSR:
"use client";
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { useEffect, useState } from "react";
interface User {
id: string;
email: string;
name: string;
}
interface AuthState {
user: User | null;
token: string | null;
setUser: (user: User | null, token: string | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
setUser: (user, token) => set({ user, token }),
logout: () => set({ user: null, token: null }),
}),
{
name: "auth-storage", // Tên key trong localStorage
storage: createJSONStorage(() => localStorage),
}
)
);
// 🛡️ Hook an toàn chống lỗi Hydration Mismatch trong Next.js
export function useHydratedAuthStore<T>(selector: (state: AuthState) => T): T | undefined {
const result = useAuthStore(selector);
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
setHydrated(true);
}, []);
return hydrated ? result : undefined;
}5. Mở rộng Store với kiến trúc Slice Pattern
Khi ứng dụng phình to, thay vì nhồi nhét tất cả vào một file store duy nhất, hãy chia nhỏ thành các Slices (như UserSlice, CartSlice, SettingSlice) rồi gộp lại vào một Root Store:
import { StateCreator } from "zustand";
export interface CartItem {
id: string;
name: string;
quantity: number;
}
export interface CartSlice {
cart: CartItem[];
addToCart: (item: CartItem) => void;
clearCart: () => void;
}
export const createCartSlice: StateCreator<CartSlice> = (set) => ({
cart: [],
addToCart: (item) =>
set((state) => ({ cart: [...state.cart, item] })),
clearCart: () => set({ cart: [] }),
});6. Phân định rõ ràng: Khi nào dùng Zustand, khi nào dùng TanStack Query?
Một sai lầm rất phổ biến của nhiều lập trình viên là dùng Zustand để lưu trữ danh sách bài viết, sản phẩm tải về từ API rồi viết thêm các state loading, error thủ công.
Hãy tuân theo quy tắc phân tách chuẩn:
| Tiêu chí | Zustand (Client State) | TanStack Query (Server State) |
|---|---|---|
| Bản chất dữ liệu | Trạng thái thuần túy của giao diện người dùng trên máy khách. | Dữ liệu thuộc quyền sở hữu của Database / Backend. |
| Ví dụ điển hình | Bật/tắt Sidebar, chọn Dark/Light mode, giỏ hàng tạm, bộ lọc lọc UI, step wizard hiện tại. | Danh sách bài viết, chi tiết sản phẩm, thông tin tài khoản user, đơn hàng. |
| Cơ chế lưu trữ | Giữ trong RAM trình duyệt, có thể lưu vào LocalStorage. | Tự động Caching, StaleTime, Invalidation, Garbage Collection. |
| Xử lý bất đồng bộ | Không cần quản lý loading / error / refetch. | Tự động quản lý isLoading, isError, refetch(), retry. |
Kết luận
Zustand là mảnh ghép hoàn hảo cho kiến trúc Front-End với Next.js App Router nhờ sự gọn nhẹ, linh hoạt, type-safe và không phụ thuộc vào React Context Provider. Khi kết hợp hài hòa giữa Zustand (Client State) và TanStack Query (Server State), bạn sẽ có một hệ thống quản lý dữ liệu cực kỳ mạnh mẽ, sạch sẽ và dễ bảo trì lâu dài.
Want to discuss further?
If you have specific questions about web performance, React, or want to collaborate on a project — reach out directly.