Zustand — "Trí Nhớ" Của Ứng Dụng (Client State Management) 🧠¶
Chào bạn! Khi làm web với React, chắc chắn bạn sẽ gặp bài toán: "Làm sao để nhớ thông tin người dùng (như tên, quyền hạn, hay việc họ đã đăng nhập chưa) và mang thông tin đó đi dùng ở mọi nơi trên web?". Phần này sẽ giới thiệu cho bạn Zustand — một công cụ quản lý trạng thái (state) siêu gọn nhẹ và cực kỳ dễ xài.
Tại Sao Lại Cần Đến "State Management"? 🤔¶
Hãy tưởng tượng việc truyền dữ liệu trong React (gọi là prop drilling) giống như trò chuyền nước bằng gáo qua một hàng người.
// Vấn đề: Cần đưa cái "Thẻ ra vào" (accessToken) cho mọi người cùng xem
function App() {
const [accessToken, setAccessToken] = useState(null);
// Bạn (App) phải đưa thẻ cho anh Bảo vệ (Layout)
return <Layout accessToken={accessToken} setAccessToken={setAccessToken} />;
}
function Layout({ accessToken, setAccessToken }) {
return (
<>
<TopNav accessToken={accessToken} />
{/* Anh Bảo vệ lại phải đưa thẻ cho chị Lễ tân (GroupPage) */}
<GroupPage accessToken={accessToken} setAccessToken={setAccessToken} />
</>
);
}
function GroupPage({ accessToken, setAccessToken }) {
// Chị Lễ tân lại phải cầm đưa tiếp cho bộ phận bên trong...
return <TransactionList accessToken={accessToken} />;
}
// → Rất cồng kềnh, mỏi tay và dễ nhầm lẫn!
Giải pháp: Chúng ta tạo ra một cái "Bảng thông báo chung" (Global state). Lưu thông tin ở giữa sảnh, ai cần gì thì cứ chạy ra đó mà tự lấy, không cần phải chuyền tay nhau nữa.
Phân Biệt: Dữ Liệu Máy Chủ vs Dữ Liệu Trình Duyệt ⚖️¶
Nhắc lại một chút kẻo nhầm lẫn nhé:
| Tiêu chí | Trí nhớ Mạng (Server State - TanStack Query) | Trí nhớ Tạm (Client State - Zustand) |
|---|---|---|
| Lấy từ đâu? | Gọi điện lên Máy chủ (API) để lấy về. | Thao tác của người dùng ngay trên màn hình. |
| Ví dụ | Danh sách giao dịch, danh sách nhóm. | Thẻ đăng nhập (Auth token), tên người dùng. |
| Đồng bộ | Luôn phải kiểm tra xem trên mạng có gì mới không. | Chỉ nhớ trong phạm vi cái trình duyệt web. |
| Nơi cất giữ | Nằm trong Database (D1). | Cất trong Ngăn bàn trình duyệt (localStorage). |
Quy tắc vàng: Dữ liệu kéo từ Internet về -> Giao cho TanStack Query. Dữ liệu chỉ dùng quanh quẩn trong trình duyệt -> Giao cho Zustand.
Cuộc Chiến: Zustand vs Redux ⚔️¶
Nếu bạn từng nghe đến Redux, thì đây là lý do chúng ta "chia tay" nó để đến với Zustand:
| Tiêu chí | Zustand 🐻 | Redux 🐘 |
|---|---|---|
| Cài đặt, viết code | Cực kỳ ngắn gọn, viết tí là xong. | Rườm rà, tạo cả chục file (actions, reducers...). |
| Độ nặng | Siêu nhẹ (~1KB). | Khá nặng đô (~15KB). |
| Công cụ hỗ trợ (DevTools) | ✅ Hỗ trợ ngon lành. | ✅ Quá nổi tiếng rồi. |
| Thời gian học | 30 phút là xài ầm ầm. | Vài ngày đến vài tuần. |
Với dự án này, ta chỉ cần lưu trữ việc Đăng nhập đơn giản, nên dùng Zustand là "chuẩn bài" nhất!
Bắt Tay Vào Làm Sổ Đăng Nhập (Auth Store) 📝¶
Dưới đây là cách chúng ta thiết lập một cuốn sổ chung để ghi nhớ xem ai đang đăng nhập. Nhìn code có vẻ dài, nhưng thực ra nó rất trong sáng:
// File: web/src/store/authStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware'; // Công cụ giúp F5 không bị mất dữ liệu
// Định nghĩa khung sổ sách (Chỉ định rõ các cột mục)
interface GroupSummary {
id: string; slug: string; name: string; currency: string;
member_role: 'admin' | 'sub_admin' | 'member';
}
interface AuthState {
// Các thông tin cần nhớ
accessToken: string | null;
userId: string | null;
name: string | null;
email: string | null;
role: 'super_admin' | 'user' | null;
groups: GroupSummary[];
// Các hành động có thể làm
setAuth: (data: /*...*/) => void;
updateGroups: (groups: GroupSummary[]) => void;
updateName: (name: string) => void;
logout: () => void;
isAuthenticated: () => boolean;
}
// Khởi tạo cuốn sổ
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
// Mặc định ban đầu là chưa có ai đăng nhập
accessToken: null, userId: null, name: null, email: null, role: null, groups: [],
// Khi đăng nhập thành công, chép thông tin vào sổ
setAuth: (data) => set({
accessToken: data.accessToken,
userId: data.userId,
name: data.name,
email: data.email,
role: data.role,
groups: data.groups ?? [],
}),
updateGroups: (groups) => set({ groups }),
updateName: (name) => set({ name }),
// Khi đăng xuất, xóa sạch thông tin
logout: () => set({
accessToken: null, userId: null, name: null, email: null, role: null, groups: [],
}),
// Hàm kiểm tra nhanh xem đã có thẻ vào cửa chưa
isAuthenticated: () => !!get().accessToken,
}),
{
name: 'quynhom-auth', // Tên của cái két sắt trong trình duyệt (Nhớ đặt cho khỏi đụng hàng)
}
)
);
Cách Dùng Sổ Chung Trong Các Màn Hình Khác 📱¶
Mở Sổ Ra Xem (Đọc dữ liệu)¶
function TopNav() {
// Đọc thông tin từ cuốn sổ chung
const { name, role, logout } = useAuthStore();
// Hoặc nếu bạn khó tính, chỉ muốn lấy đúng 1 thứ (Tốt cho hiệu suất web)
// const name = useAuthStore(state => state.name);
return (
<nav>
<span>Xin chào, {name}</span>
{role === 'super_admin' && <Link to="/admin">Trang Quản Lý</Link>}
<button onClick={logout}>Đăng xuất</button>
</nav>
);
}
Ghi Chép Vào Sổ (Sau khi đăng nhập)¶
function LoginPage() {
const setAuth = useAuthStore(state => state.setAuth);
const handleLogin = async (credentials) => {
// Xin API đăng nhập
const result = await api.auth.login(credentials);
// Bê nguyên kết quả ghi vào Sổ chung
setAuth({
accessToken: result.accessToken,
userId: result.userId,
name: result.name,
email: result.email,
role: result.role,
groups: result.groups,
});
// Đá khách sang trang Danh sách nhóm
navigate('/groups');
};
}
Kẹp Thẻ Đi Gọi API¶
function GroupPage() {
// Moi cái thẻ từ trong túi (Sổ chung) ra
const accessToken = useAuthStore(state => state.accessToken);
const { data } = useQuery({
queryKey: ['group', groupId],
queryFn: () => api.groups.get(groupId!, accessToken!), // Kẹp thẻ vào đưa cho bảo vệ API
enabled: !!accessToken, // Chỉ chạy khi cầm thẻ trên tay
});
}
Phép Thuật Giữ Chân Khách (Persist Middleware) 🪄¶
Theo mặc định, nếu bạn ấn nút F5 (Tải lại trang), trình duyệt sẽ xóa sạch trí nhớ, bạn sẽ bị văng ra khỏi hệ thống.
Nhưng nhờ có chữ persist mà ta lồng vào code ở trên, Zustand sẽ tự động làm thao tác lưu nháp:
// Lúc có người đăng nhập:
// Nó lén mở ngăn kéo trình duyệt (localStorage) ra cất thông tin vào
// Lúc bạn vừa F5 lại web:
// Nó lập tức chạy vào ngăn kéo, moi thông tin cũ ra đắp lại vào sổ
Kết quả: Khách F5 lại trang vẫn nằm im đó, không phải gõ mật khẩu lại từ đầu!
Lưu Ý Bảo Mật 🚨
Như đã phân tích ở phần Authentication, cất thẻ trong localStorage có nguy cơ bị hacker chèn mã độc (XSS attack) móc trộm thẻ. Nhưng vì web của chúng ta không lưu thẻ tín dụng ngân hàng thật nên sự đánh đổi này là chấp nhận được để lấy sự tiện lợi.
Vậy Khi Nào Thì Không Nên Dùng Zustand? 🚫¶
Cuốn sổ chung rất tốt, nhưng không phải cái gì cũng lôi ra giữa làng mà ghi. Những thứ mang tính chất cá nhân, dùng xong vứt luôn thì hãy dùng useState thông thường:
// ✅ Chuẩn: Dùng useState cho những thứ lặt vặt chỉ xuất hiện trên cái khung Modal này thôi
function AddTransactionModal() {
const [amount, setAmount] = useState('');
const [description, setDescription] = useState('');
const [activeTab, setActiveTab] = useState<'income' | 'expense'>('expense');
// ...
}
// ✅ Chuẩn: Dùng Zustand cho những thứ quan trọng, cần nhiều người biết và cần nhớ sau khi F5
export const useAuthStore = create<AuthState>()(persist(...));
Thần chú chốt hạ: Chuyện nhỏ trong nhà tự đóng cửa bảo nhau -> useState. Chuyện lớn cả làng cần biết -> Zustand.