Tại Sao Tôi Ngừng Dùng Redux Cho Các Dự Án Mới
Khoảng tám tháng trước, tôi tiếp nhận một dashboard React tầm trung đang chạy Redux Toolkit. Nó hoạt động ổn — state có thể đoán trước được, DevTools rất xuất sắc, và cả team đã quen với các pattern. Nhưng mỗi khi cần thêm tính năng mới, dù đơn giản như theo dõi trạng thái mở của một modal, chúng tôi vẫn phải viết một slice, thêm actions, kết nối selectors, và chạm vào ít nhất bốn file. Chỉ để quản lý một cái cờ loading toàn cục.
Sự rườm rà đó thúc đẩy tôi tìm kiếm giải pháp thay thế. Sau khi thử nghiệm Jotai, Recoil và Zustand trên các dự án phụ, tôi đã migrate một trong các công cụ nội bộ sang Zustand. Sáu tháng sau, nó vẫn đang chạy production mà không có sự cố nào liên quan đến state. Đây là những gì tôi học được.
Redux vs Zustand: So Sánh Hai Hướng Tiếp Cận
Về cốt lõi, cả hai thư viện đều giải quyết cùng một vấn đề — chia sẻ state giữa các component mà không cần prop drilling. Sự khác biệt nằm ở mức độ nghi lễ mà chúng yêu cầu để đạt được điều đó.
Redux (với Redux Toolkit)
Redux thực thi luồng dữ liệu một chiều nghiêm ngặt. Bạn định nghĩa các slice với reducers và actions, dispatch những actions đó từ components, và đọc state qua selectors. Redux Toolkit cắt giảm đáng kể boilerplate so với Redux thuần, nhưng mental model vẫn yêu cầu hiểu actions, reducers, store và chu trình dispatch.
Trên các team lớn với luồng async phức tạp, cấu trúc đó xứng đáng. Tính có thể đoán trước và hỗ trợ DevTools rất khó sánh kịp. Nhưng với các team nhỏ hơn — hoặc cho những tính năng mà chi phí không được biện minh — thì giống như mặc giáp sắt đầy đủ để đi mua sắm tạp hóa.
Zustand
Zustand loại bỏ hoàn toàn mô hình đó. Bạn tạo một store như một đối tượng JavaScript thuần — state và actions cùng tồn tại với nhau. Components subscribe bằng một hook và chỉ re-render khi phần state cụ thể mà chúng sử dụng thay đổi.
Không có Provider. Không có dispatch. Không có action types. Bạn gọi hàm trực tiếp. Cảm giác gần hơn với việc quản lý state bằng useState và useContext, nhưng không có những vấn đề về hiệu năng do context re-renders trên cây component lớn.
Ưu và Nhược Điểm Sau Khi Dùng Thực Tế Trên Production
Ưu Điểm của Zustand
- Boilerplate tối thiểu: Một store hoàn chỉnh với async actions gói gọn trong chưa đến 30 dòng. Với Redux Toolkit, bạn sẽ cần một file slice, async thunks và định nghĩa selector trải rộng trên nhiều vị trí.
- Không cần Provider: Store tồn tại ở cấp module. Import và dùng ở bất kỳ đâu — utility functions, event handlers, thậm chí môi trường test Node.js — mà không cần bọc app trong bất cứ thứ gì.
- Subscription có chọn lọc: Components chỉ re-render khi state cụ thể mà chúng subscribe thay đổi. Không cần phải vật lộn với memoization. Chính điều này đã khắc phục hai vấn đề regression hiệu năng mà chúng tôi gặp trong dashboard Redux.
- TypeScript không gây khó dễ: Định kiểu cho một Zustand store rất đơn giản. Các kiểu generic của Redux có thể leo thang thành độ phức tạp thực sự, đặc biệt với các shape state lồng nhau.
- Bundle siêu nhỏ: Zustand chỉ ~1KB sau khi gzip. Redux Toolkit nặng ~11KB. Không phải vấn đề lớn khi đứng một mình, nhưng sẽ cộng dồn theo thời gian.
Hạn Chế của Zustand
- DevTools không tự động có: Bạn phải thêm middleware
devtoolsthủ công. Tích hợp Redux DevTools được đánh bóng hơn ngay từ đầu — lịch sử action, diff views, tất cả mọi thứ. - Không có cấu trúc bắt buộc: Sự tự do cũng là cái bẫy. Nếu không có quy ước của team, các store có thể biến thành mớ hỗn độn khi codebase phát triển.
- Time-travel debugging: Redux vẫn thắng ở đây. Zustand hỗ trợ tính năng này qua middleware devtools, nhưng trải nghiệm kém trưởng thành hơn.
- Hệ sinh thái middleware nhỏ hơn: Redux có hỗ trợ middleware sâu — logging, persistence, orchestration dựa trên saga. Zustand có
persistvàimmer, đủ xử lý hầu hết các trường hợp, nhưng hệ sinh thái mỏng hơn.
Cấu Hình Được Đề Xuất Cho Dự Án Production
Sau sáu tháng, đây là cấu hình đã trụ vững nhất qua ba dự án khác nhau.
Cài Đặt
npm install zustand
# Tùy chọn nhưng được khuyến nghị để cập nhật immutable:
npm install immer
Cấu Trúc Thư Mục
Đặt các store trong một thư mục store/ riêng. Một file cho mỗi khái niệm domain. Đừng nhồi nhét tất cả vào một file store duy nhất — đó là cách bạn kết thúc với những con quái vật 400 dòng sau sáu tháng.
src/
store/
useAuthStore.ts
useCartStore.ts
useNotificationStore.ts
Bật DevTools
Luôn bọc store creator với middleware devtools trong môi trường development. Bạn sẽ tự cảm ơn mình lần đầu tiên khi đang săn lùng một bug state tinh vi:
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const useAuthStore = create(devtools((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
})))
export default useAuthStore
Hướng Dẫn Triển Khai: Từ Cơ Bản Đến Async
Store Cơ Bản
Bắt đầu từ đây — một bộ đếm. Quá đơn giản? Đúng vậy. Nhưng nó thể hiện đầy đủ pattern một cách rõ ràng trước khi async và middleware xuất hiện:
import { create } from 'zustand'
interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))
Và trong một component:
import { useCounterStore } from '../store/useCounterStore'
export function Counter() {
const { count, increment, decrement } = useCounterStore()
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}
Async Actions và Gọi API
Không cần middleware cho async. Actions chỉ là các hàm thông thường, nên async/await hoạt động trực tiếp:
import { create } from 'zustand'
interface User {
id: number
name: string
email: string
}
interface UserState {
users: User[]
isLoading: boolean
error: string | null
fetchUsers: () => Promise<void>
}
export const useUserStore = create<UserState>((set) => ({
users: [],
isLoading: false,
error: null,
fetchUsers: async () => {
set({ isLoading: true, error: null })
try {
const res = await fetch('/api/users')
const data = await res.json()
set({ users: data, isLoading: false })
} catch (err) {
set({ error: 'Không thể tải danh sách người dùng', isLoading: false })
}
},
}))
Subscription Có Chọn Lọc Để Tối Ưu Hiệu Năng
Chỉ lấy những gì bạn cần từ store. Mỗi component chỉ re-render khi slice cụ thể của nó thay đổi — không phải khi bất kỳ phần nào của store thay đổi:
// Chỉ re-render khi `isLoading` thay đổi
function LoadingIndicator() {
const isLoading = useUserStore((state) => state.isLoading)
return isLoading ? <Spinner /> : null
}
// Chỉ re-render khi `users` thay đổi
function UserList() {
const users = useUserStore((state) => state.users)
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
Lưu State Qua Các Lần Tải Lại Trang
Middleware persist tích hợp sẵn xử lý việc đồng bộ với localStorage hoặc sessionStorage. Tùy chọn giao diện sáng/tối là trường hợp sử dụng điển hình:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface ThemeState {
theme: 'light' | 'dark'
toggleTheme: () => void
}
export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}),
{ name: 'theme-storage' } // key trong localStorage
)
)
Đọc State Store Bên Ngoài React
Một trong những pattern tôi dùng nhiều nhất — truy cập state store trong axios interceptors hoặc utility functions mà không cần hooks:
// Gắn auth token vào mọi request gửi đi
import { useAuthStore } from '../store/useAuthStore'
axios.interceptors.request.use((config) => {
const token = useAuthStore.getState().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
Khi Nào Vẫn Nên Chọn Redux
Zustand không phải là sự thay thế hoàn toàn cho mọi trường hợp sử dụng Redux. Trên các ứng dụng quy mô lớn với 10+ developer, các quy ước bắt buộc của Redux Toolkit và DevTools trưởng thành vẫn xứng đáng với chi phí bỏ ra. Nếu bạn đang điều phối các luồng async nhiều bước với Redux-Saga — như pipeline thanh toán, form submission nhiều giai đoạn, logic retry phức tạp — hệ sinh thái middleware Redux đi sâu hơn nhiều.
Với mọi thứ còn lại — SPA, dashboard, công cụ nội bộ, hầu hết các ứng dụng hướng khách hàng dưới vài trăm nghìn dòng — Zustand loại bỏ sự rườm rà mà không giảm bớt khả năng. Sáu tháng trên production, ba dự án, không có sự cố nào liên quan đến state. Ít file phải chạm vào hơn mỗi khi thêm tính năng. Onboarding nhanh hơn cho thành viên mới. Độ tin cậy tương đương.
Hãy bắt đầu nhỏ. Migrate một feature slice. Xem cảm giác thế nào sau một tuần. Thường thì chừng đó là đủ để quyết định trở nên rõ ràng.

