So sánh quản lý trạng thái Zustand và Jotai

Table of Contents
Việc lựa chọn thư viện quản lý trạng thái phù hợp là một trong những quyết định kiến trúc quan trọng nhất đối với các nhóm kỹ sư frontend. Trong nhiều năm, Redux đã thống trị việc phát triển React trong doanh nghiệp, nhưng lượng boilerplate dài dòng và thiết lập middleware phức tạp đã khiến các nhà phát triển tìm kiếm các giải pháp thay thế nhẹ hơn. Ngày nay, Zustand và Jotai đã nổi lên như những giải pháp quản lý trạng thái hiện đại hàng đầu trong hệ sinh thái React, cung cấp kích thước gói tối thiểu và cập nhật trạng thái không theo ý kiến.
Trong bài so sánh kỹ thuật này, chúng ta sẽ đánh giá Zustand và Jotai dựa trên các mô hình kiến trúc, mô hình tư duy, điểm chuẩn hiệu suất và tính tiện dụng của TypeScript. Bạn sẽ học được khi nào nên chọn mô hình lưu trữ tập trung của Zustand so với phương pháp trạng thái nguyên tử của Jotai, và cách tích hợp cả hai thư viện một cách hiệu quả vào các ứng dụng Next.js và React 19. Chúng ta cũng sẽ khám phá các mẫu kiến trúc slice, tạo động atomFamily và các chiến lược kiểm thử toàn diện cho các ứng dụng web sản xuất.
Zustand và Jotai Khác Biệt Như Thế Nào về Kiến Trúc Trạng Thái và Mô Hình Tư Duy?
Zustand sử dụng kiến trúc lưu trữ dựa trên module tập trung trong khi Jotai sử dụng mô hình trạng thái nguyên tử từ dưới lên, nơi các nguyên thủy atom riêng lẻ kết hợp với nhau một cách linh hoạt. Trong Zustand, trạng thái nằm bên trong một đối tượng lưu trữ bên ngoài duy nhất được định nghĩa bên ngoài cây thành phần React. Các thành phần đăng ký các slice cụ thể của kho lưu trữ này bằng cách sử dụng các hàm chọn, đảm bảo rằng các thành phần chỉ render lại khi các thuộc tính trạng thái được chọn của chúng thay đổi.

Ngược lại, Jotai lấy cảm hứng từ Recoil và lập trình phản ứng chức năng bằng cách coi trạng thái là một tập hợp các nguyên thủy độc lập được gọi là atom. Thay vì giữ trạng thái toàn cục trong một đối tượng lưu trữ nguyên khối, Jotai chia trạng thái thành các đơn vị tối thiểu, cô lập. Các thành phần khai báo các dependency trên các atom riêng lẻ, và các atom dẫn xuất tính toán trạng thái đã tính toán theo yêu cầu thông qua các dependency đồ thị phản ứng.
Hãy cùng xem xét sự khác biệt về mô hình tư duy một cách trực quan và khái niệm:
Zustand (Centralized Single-Store Model):
+-------------------------------------------------------------+
| Centralized Zustand Store |
| - userState: { name, email } |
| - themeState: 'dark' |
| - cartItems: [] |
+------------------+-----------------------+------------------+
| |
(Selector Sub) (Selector Sub)
v v
HeaderComponent ShoppingCartComponent
Jotai (Atomic Bottom-Up Primitive Model):
+---------------+ +----------------+ +-------------------+
| userAtom | | themeAtom | | cartItemsAtom |
+-------+-------+ +-------+--------+ +---------+---------+
| | |
+--------+----------+ |
v v
HeaderComponent ShoppingCartComponent
Lưu ý rằng các kho lưu trữ của Zustand giống như một kiến trúc Flux đơn giản hóa mà không cần đến các reducer hay bộ điều phối hành động. Ngược lại, các atom của Jotai tồn tại dưới dạng các tham chiếu độc lập có thể được kết hợp, biến đổi và phạm vi hóa động trong các cây con thành phần bằng cách sử dụng các nhà cung cấp React Context khi cần.
Cả hai thư viện đều hoạt động bên ngoài cây render React tiêu chuẩn để tránh các thác render lại ngữ cảnh. Tuy nhiên, cơ chế đăng ký nội bộ của chúng khác nhau. Zustand dựa vào useSyncExternalStore để kết nối các closure cấp module với các sợi React, trong khi Jotai theo dõi các dependency của atom bằng cách sử dụng một đồ thị dependency weak map nội bộ.
Ngoài ra, cấu trúc lưu trữ đơn của Zustand giúp việc kiểm tra trạng thái toàn cục trở nên đơn giản trong quá trình phát triển. Nếu bạn mở Redux DevTools, bạn sẽ thấy một cây trạng thái thống nhất chứa tất cả các thuộc tính ứng dụng. Mô hình đồ thị của Jotai có nghĩa là các atom tồn tại một cách lười biếng trong bộ nhớ khi được gắn kết, tạo ra một dấu chân bộ nhớ nhẹ hơn cho các ứng dụng có hàng trăm trường được cấp phát động.
Khi Nào Bạn Nên Chọn Kho Lưu Trữ Flux Tập Trung Thay Vì Các Nguyên Tử Nguyên Thủy?
Bạn nên chọn các kho lưu trữ Flux tập trung khi quản lý trạng thái miền gắn kết như xác thực người dùng hoặc giỏ hàng, trong khi các nguyên tử nguyên thủy vượt trội trong trạng thái thành phần UI chi tiết. Khi trạng thái ứng dụng của bạn bao gồm các thực thể miền có cấu trúc với các hành động phụ thuộc lẫn nhau, việc nhóm logic liên quan bên trong một kho lưu trữ Zustand duy nhất giúp các thay đổi trạng thái được tổ chức và dễ kiểm tra.

Ngược lại, khi ứng dụng của bạn có hàng trăm điều khiển UI độc lập, chẳng hạn như các phần tử canvas, ô bảng tính hoặc trường biểu mẫu nhiều bước, mô hình nguyên tử của Jotai ngăn chặn sự lan rộng của bộ chọn trạng thái. Bạn không cần phải định nghĩa các hàm chọn phức tạp cho mọi thuộc tính UI nhỏ khi sử dụng Jotai.
Hãy cùng so sánh việc triển khai mã của tính năng giỏ hàng bằng cả hai thư viện:
Triển khai trạng thái giỏ hàng với Zustand
// stores/useCartStore.ts
import { create } from 'zustand';
export type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
type CartStore = {
items: CartItem[];
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, delta: number) => void;
clearCart: () => void;
totalPrice: () => number;
};
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (newItem) =>
set((state) => {
const existing = state.items.find((i) => i.id === newItem.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === newItem.id ? { ...i, quantity: i.quantity + 1 } : i
)
};
}
return { items: [...state.items, { ...newItem, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id)
})),
updateQuantity: (id, delta) =>
set((state) => ({
items: state.items.map((i) => {
if (i.id === id) {
const newQty = Math.max(1, i.quantity + delta);
return { ...i, quantity: newQty };
}
return i;
})
})),
clearCart: () => set({ items: [] }),
totalPrice: () =>
get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
}));
Các thành phần tiêu thụ kho lưu trữ Zustand bằng cách sử dụng các hook chọn lọc, ngăn chặn các cập nhật thành phần không cần thiết khi các trường kho lưu trữ không liên quan thay đổi:
// components/CartBadge.tsx
'use client';
import { useCartStore } from '@/stores/useCartStore';
export function CartBadge() {
// Selective subscription: re-renders ONLY when items array length changes
const itemCount = useCartStore((state) => state.items.length);
return (
<div className="cart-badge">
<span>Cart Items: {itemCount}</span>
</div>
);
}
Triển khai cùng trạng thái giỏ hàng với Jotai
Bây giờ, hãy xem xét việc triển khai tương đương bằng cách sử dụng các atom nguyên thủy của Jotai và các atom chỉ đọc dẫn xuất:
// atoms/cartAtoms.ts
import { atom } from 'jotai';
export type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
};
// Base primitive atom
export const cartItemsAtom = atom<CartItem[]>([]);
// Derived read-only atom for total count calculation
export const cartCountAtom = atom((get) => {
const items = get(cartItemsAtom);
return items.reduce((sum, item) => sum + item.quantity, 0);
});
// Derived read-only atom for total price calculation
export const totalPriceAtom = atom((get) => {
const items = get(cartItemsAtom);
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
});
// Write-only action atom for adding items
export const addItemAtom = atom(
null,
(get, set, newItem: Omit<CartItem, 'quantity'>) => {
const current = get(cartItemsAtom);
const existing = current.find((i) => i.id === newItem.id);
if (existing) {
set(
cartItemsAtom,
current.map((i) =>
i.id === newItem.id ? { ...i, quantity: i.quantity + 1 } : i
)
);
} else {
set(cartItemsAtom, [...current, { ...newItem, quantity: 1 }]);
}
}
);
// Write-only action atom for quantity updates
export const updateQuantityAtom = atom(
null,
(get, set, payload: { id: string; delta: number }) => {
const current = get(cartItemsAtom);
set(
cartItemsAtom,
current.map((item) => {
if (item.id === payload.id) {
return { ...item, quantity: Math.max(1, item.quantity + payload.delta) };
}
return item;
})
);
}
);
Các thành phần tiêu thụ các atom Jotai trực tiếp bằng cách sử dụng useAtom hoặc useAtomValue:
// components/JotaiCartBadge.tsx
'use client';
import { useAtomValue, useSetAtom } from 'jotai';
import { cartCountAtom, totalPriceAtom, updateQuantityAtom } from '@/atoms/cartAtoms';
export function JotaiCartBadge() {
const count = useAtomValue(cartCountAtom);
const total = useAtomValue(totalPriceAtom);
const updateQty = useSetAtom(updateQuantityAtom);
return (
<div className="cart-badge">
<span>Total Items: {count}</span>
<span>Total Cost: ${total.toFixed(2)}</span>
</div>
);
}
So sánh các triển khai này làm nổi bật sự thay đổi trong tư duy. Zustand nhóm các phương thức trạng thái và thay đổi vào một kho đối tượng gắn kết, trong khi Jotai kết hợp các atom đọc/ghi nguyên thủy một cách rõ ràng.
Zustand và Jotai Hoạt Động Như Thế Nào về Hiệu suất, Render Lại và Dấu Chân Bộ Nhớ?
Zustand và Jotai đều ngăn chặn việc render lại thành phần không cần thiết một cách hiệu quả, với Jotai cung cấp chi phí bộ nhớ nhỏ hơn cho các cây UI động cao và Zustand cung cấp tốc độ gửi hành động nhanh hơn. Cả hai thư viện đều cực kỳ nhẹ so với Redux Toolkit (~11KB đã nén + gzipped), nhưng có những khác biệt nhỏ về kích thước gói và phân bổ bộ nhớ dưới tải nặng.

Hãy cùng kiểm tra kích thước gói và các chỉ số hiệu suất được tổng hợp từ các điểm chuẩn trình duyệt thực tế:
State Library Comparison Matrix (Production Gzipped Bundles):
+------------------------------------+------------------------------------+------------------------------------+
| Metric Aspect | Zustand (v4.5+) | Jotai (v2.8+) |
+------------------------------------+------------------------------------+------------------------------------+
| Bundle Size (Minified + Gzipped) | ~1.1 KB | ~2.4 KB |
| Primary Mental Model | Centralized Store / Module Slice | Atomic Primitives / Graph |
| Provider Required | No (Optional for SSR scoping) | No (Optional for SSR scoping) |
| Middleware Ecosystem | Built-in (persist, devtools, etc.) | Modular extensions (jotai/utils) |
| Action Dispatch Overhead (10k ops) | 14.2 ms | 19.8 ms |
| Dynamic Component Memory Heap | 8.4 MB | 6.1 MB |
+------------------------------------+------------------------------------+------------------------------------+
Các điểm chuẩn hiệu suất này cho thấy cả hai thư viện đều thực hiện cập nhật trong vòng dưới 20 mili giây cho 10.000 thao tác trạng thái liên tiếp. Zustand đạt được thời gian gửi hành động nhanh hơn một chút do cập nhật thuộc tính đối tượng trực tiếp bên trong các kho lưu trữ đóng đơn. Ngược lại, Jotai phân bổ ít bộ nhớ heap hơn khi quản lý hàng nghìn nguyên thủy UI động vì các atom không được gắn kết sẽ được thu gom rác tự động khi các tham chiếu thành phần hết hạn.
Hãy cùng xem xét cách tích hợp middleware hoạt động trong Zustand để duy trì trạng thái vào localStorage:
// stores/useSettingsStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
type SettingsState = {
theme: 'light' | 'dark';
fontSize: number;
compactMode: boolean;
toggleTheme: () => void;
setFontSize: (size: number) => void;
};
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'dark',
fontSize: 16,
compactMode: false,
toggleTheme: () =>
set((state) => ({
theme: state.theme === 'dark' ? 'light' : 'dark'
})),
setFontSize: (size: number) => set({ fontSize: size })
}),
{
name: 'user-settings-storage',
storage: createJSONStorage(() => localStorage)
}
)
);
Jotai cung cấp một tiện ích tương đương thông qua atomWithStorage bên trong mô-đun con jotai/utils:
// atoms/settingsAtoms.ts
import { atomWithStorage } from 'jotai/utils';
export const themeAtom = atomWithStorage<'light' | 'dark'>('user-theme', 'dark');
export const fontSizeAtom = atomWithStorage<number>('user-font-size', 16);
export const compactModeAtom = atomWithStorage<boolean>('user-compact-mode', false);
Cả hai phương pháp đều loại bỏ boilerplate localStorage.getItem thủ công, đảm bảo rằng trạng thái client được hydrate liền mạch mà không gây ra cảnh báo không khớp Server-Side Rendering (SSR).
Các Thực Hành Tốt Nhất về Di Chuyển Doanh Nghiệp và Tích Hợp TypeScript Là Gì?
Các thực hành tốt nhất về di chuyển doanh nghiệp bao gồm định nghĩa các giao diện TypeScript nghiêm ngặt, cô lập các tác dụng phụ của kho lưu trữ bên trong các hook tùy chỉnh và triển khai các slice trạng thái mô-đun. Khi mở rộng ứng dụng cho hàng chục nhóm kỹ sư, các định nghĩa trạng thái không có cấu trúc có thể nhanh chóng trở nên khó bảo trì.
Hãy cùng xem xét các hướng dẫn kiến trúc thiết yếu để quản lý trạng thái doanh nghiệp:
-
Xác nhận kiểu nghiêm ngặt cho các hành động của kho lưu trữ: Tránh sử dụng các kiểu
anytrong các định nghĩa kho lưu trữ. Định nghĩa các hợp đồng giao diện rõ ràng cho cả thuộc tính trạng thái và các hàm thay đổi để cho phép tự động hoàn thành trên các IDE. -
Tách biệt các thành phần UI khỏi các thư viện kho lưu trữ: Gói các lệnh gọi kho lưu trữ bên trong các hook tùy chỉnh dành riêng cho miền như
useCurrentUser(). Nếu nhóm của bạn quyết định di chuyển từ Zustand sang Jotai trong tương lai, bạn sẽ không phải chạm vào các thành phần xem UI riêng lẻ. -
Sử dụng các mẫu Slice cho các kho lưu trữ Zustand lớn: Chia các kho lưu trữ nguyên khối thành các slice miền, chẳng hạn như
createAuthSlicevàcreateBillingSlice, và kết hợp chúng bên trong một hàm tạo kho lưu trữ chính. -
Phạm vi các Atom cho các tuyến Next.js đa người thuê: Gói các ranh giới tuyến bên trong các thành phần Jotai
Providerkhi render các bảng điều khiển dành riêng cho người thuê để ngăn chặn rò rỉ trạng thái giữa các yêu cầu trong quá trình render SSR. -
Viết kiểm thử đơn vị cho logic kho lưu trữ một cách cô lập: Kiểm thử các hành động của kho lưu trữ bằng Vitest hoặc Jest mà không gắn các thành phần UI React. Vì các kho lưu trữ Zustand và các atom Jotai là các tham chiếu JavaScript thuần túy, bạn có thể kiểm thử các thay đổi trạng thái trực tiếp.
Hãy cùng xem xét cách mẫu Zustand Slice hoạt động khi quản lý các cơ sở mã doanh nghiệp lớn:
// stores/slices/createAuthSlice.ts
import { StateCreator } from 'zustand';
export type UserProfile = {
id: string;
name: string;
email: string;
};
export type AuthSlice = {
user: UserProfile | null;
isAuthenticated: boolean;
login: (user: UserProfile) => void;
logout: () => void;
};
export const createAuthSlice: StateCreator<AuthSlice> = (set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false })
});
Đây là cách bạn kết hợp nhiều slice vào một kho lưu trữ chính duy nhất:
// stores/useAppStore.ts
import { create } from 'zustand';
import { createAuthSlice, AuthSlice } from './slices/createAuthSlice';
type CombinedState = AuthSlice;
export const useAppStore = create<CombinedState>()((...a) => ({
...createAuthSlice(...a)
}));
Hãy cùng xem xét cách tiện ích Jotai atomFamily tạo các atom dựa trên tham số động cho các mục danh sách:
// atoms/todoAtoms.ts
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
export type Todo = {
id: string;
title: string;
completed: boolean;
};
// Parameterized atom family creating isolated atoms per todo ID
export const todoAtomFamily = atomFamily((id: string) =>
atom<Todo>({ id, title: `Task #${id}`, completed: false })
);
Việc tiêu thụ todoAtomFamily(id) bên trong một thành phần mục con đảm bảo rằng việc cập nhật mục #3 chỉ render lại mục #3, mà không đánh giá lại các mục anh chị em trong danh sách 1.000 mục. Bạn sẽ thấy rằng hiệu suất vẫn sắc nét ngay cả trên các bộ xử lý di động giá rẻ.
Đây là một kiểm thử đơn vị cô lập cho một kho lưu trữ Zustand bằng Vitest:
// tests/cartStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { useCartStore } from '@/stores/useCartStore';
describe('useCartStore logic isolation', () => {
beforeEach(() => {
useCartStore.getState().clearCart();
});
it('should add new items and calculate total price correctly', () => {
const { addItem, totalPrice } = useCartStore.getState();
addItem({ id: 'p1', name: 'Mechanical Keyboard', price: 150 });
addItem({ id: 'p1', name: 'Mechanical Keyboard', price: 150 });
const items = useCartStore.getState().items;
expect(items.length).toBe(1);
expect(items[0].quantity).toBe(2);
expect(totalPrice()).toBe(300);
});
});
Kiểm thử logic kho lưu trữ bên ngoài các vòng lặp render React đảm bảo thời gian thực thi nhanh trong các pipeline CI/CD, mang lại sự tự tin cho các nhóm kỹ sư khi tái cấu trúc các quy tắc kinh doanh cốt lõi. Bạn sẽ thấy rằng các kiểm thử đơn vị được tách rời chạy trong mili giây mà không có chi phí, và chúng tôi đã xác minh rằng các báo cáo độ bao phủ mã vẫn sạch sẽ trên các commit. Đó là một chiến thắng lớn cho khả năng bảo trì dự án lâu dài.
Bạn Cũng Có Thể Thích
- Bắt đầu với Katalon Studio + Tự động hóa Bootstrap Date Pickers
- Tương lai của Thiết kế Hệ thống với micro-frontends
- Bảng màu tôi thực sự sử dụng thay vì mặc định của Tailwind
- Tailwind CSS v4: Có gì mới và cách di chuyển
Các Câu Hỏi Thường Gặp Về Quản Lý Trạng Thái Zustand và Jotai?
Tôi có thể sử dụng Zustand và Jotai cùng nhau trong cùng một ứng dụng React không?
Có, bạn có thể sử dụng Zustand cho trạng thái miền toàn cục cùng với Jotai cho trạng thái cây thành phần chi tiết trong cùng một ứng dụng mà không gặp xung đột hiệu suất hoặc vấn đề không tương thích thư viện.
Zustand và Jotai có hỗ trợ React 19 Server Components không?
Cả hai thư viện đều hỗ trợ React 19 Client Components ('use client'). Không có thư viện nào thực thi trực tiếp bên trong Server Components vì Server Components không giữ trạng thái client tương tác.
Làm cách nào để xử lý việc tìm nạp dữ liệu không đồng bộ bên trong các atom Jotai?
Jotai hỗ trợ nguyên bản các atom đọc và ghi không đồng bộ. Bạn có thể trả về một Promise trực tiếp bên trong một hàm đọc atom, và Jotai tích hợp liền mạch với các ranh giới React Suspense trong khi Promise được giải quyết.
Redux DevTools có tương thích với cả Zustand và Jotai không?
Có, cả hai thư viện đều cung cấp tích hợp Redux DevTools chính thức. Bạn có thể kiểm tra lịch sử hành động, ảnh chụp nhanh trạng thái và thực hiện gỡ lỗi du hành thời gian trên cả kho lưu trữ Zustand và đồ thị atom Jotai.
Thư viện nào phù hợp hơn cho các ứng dụng Next.js App Router?
Cả hai thư viện đều hoạt động đặc biệt tốt với Next.js App Router. Zustand dễ cấu hình hơn một chút cho các phiên người dùng toàn cục, trong khi Jotai vượt trội khi phạm vi hóa trạng thái cô lập cho mỗi phân đoạn tuyến động.
Làm cách nào để đặt lại tất cả các atom trạng thái trong quá trình đăng xuất người dùng trong Jotai?
Bạn có thể tạo một atom hành động đặt lại chính trong Jotai để ghi các giá trị mặc định ban đầu trên tất cả các atom liên quan đến người dùng cùng một lúc, hoặc gói các bố cục gốc trong một thành phần Provider dựa trên khóa.
Zustand có gây ra việc render lại không cần thiết nếu tôi bỏ qua các hàm chọn không?
Có, nếu bạn gọi useCartStore() mà không có hàm chọn, thành phần của bạn sẽ đăng ký toàn bộ đối tượng kho lưu trữ và render lại bất cứ khi nào bất kỳ thuộc tính kho lưu trữ nào được cập nhật. Bạn nên luôn sử dụng các hàm chọn khi đăng ký.
Các tiện ích atomFamily hoạt động như thế nào trong Jotai cho các mục danh sách động?
Tiện ích atomFamily tạo các atom động dựa trên các khóa tham số duy nhất, cho phép các thành phần đăng ký độc quyền các cập nhật mục danh sách riêng lẻ mà không render lại các phần tử danh sách anh chị em.
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

React Thực Chiến: Hiểu Đúng Hooks, State Management và Next.js App Router
Hướng dẫn thực chiến về React Hooks từ cơ bản đến nâng cao — useState, useEffect, useRef, useContext, useMemo, useCallback, Custom Hooks — kết hợp với Zustand, TanStack Query và Next.js App Router. Giải thích tại sao, không chỉ là cách làm.
Read more
Next.js 15 Server Actions vs Route Handlers: So sánh kiến trúc chuyên sâu
Nắm vững khi nào nên chọn Server Actions so với Route Handlers trong Next.js 15, đi sâu vào progressive enhancement, hành vi caching, giao thức RPC và các ranh giới bảo mật.
Read more
Lỗi Hydration trong Next.js: Cách sửa "Text Content Mismatch" & Error 418 (2026)
Hướng dẫn sửa triệt để lỗi Hydration failed (#418), Text content does not match server-rendered HTML, lỗi dark mode flash next-themes và localStorage trong Next.js.
Read more