· zustand / redux / redux-toolkit

Zustand vs Redux Toolkit — đánh giá quản lý state 2026

Dự án mới? Dùng Zustand. Codebase đang dùng RTK? Đừng migrate chỉ vì trend. Số liệu benchmark, đánh đổi thực tế, và kết luận từ một senior dev.

Bởi

2.055 từ · 11 phút đọc

Dùng Zustand cho dự án mới. Nếu codebase đang chạy Redux Toolkit, đừng rewrite — RTK xứng đáng với công sức bỏ ra khi scale. Đó là kết luận. Phần còn lại của bài này giải thích rõ khi nào kết luận đó đúng và khi nào nó lật ngược.

Bài này dành cho ai

Developer React đang có codebase RTK và tự hỏi liệu migrate có đáng không. Và cả engineer bắt đầu dự án mới từ đầu, từng bị boilerplate Redux làm khổ và muốn biết Zustand có thực sự khác không.

Nếu bạn đang build với Electron hoặc ngoài React — Zustand không giả định môi trường renderer nên vẫn dùng được, nhưng so sánh với RTK lúc này gần như không liên quan.

Chúng tôi đã test gì

  • Zustand v5.0.13 (phát hành 2025-05-05), test trên React 18.3 và React 19 RC
  • Redux Toolkit v2.12.0 với react-redux v9.x
  • Cả hai đều yêu cầu React 18+ làm peer dependency
  • Bundle phân tích qua Bundlephobia (minified + gzip): Zustand ~2.1 KB, RTK + react-redux ~15–19 KB tùy import
  • Parse time với 4× CPU throttle: Zustand 8 ms, RTK 34 ms — nhất quán với khoảng cách về kích thước
  • Lượt download hàng tuần theo npm trends: Zustand vượt RTK năm 2025, hiện có 58k GitHub stars so với RTK 11k
  • State of React 2024 survey: tỉ lệ dùng Zustand tăng từ 28% → 41% YoY, dẫn đầu về “positivity” chỉ sau useState

Phân tích

Boilerplate mới là thứ tạo ra khoảng cách thực sự

RTK slice approach:

// store/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const initialState: CounterState = { value: 0 }

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => { state.value += 1 },
    decrement: (state) => { state.value -= 1 },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

// store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: { counter: counterReducer },
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

// App.tsx — kết nối
import { Provider } from 'react-redux'
import { store } from './store'

function App() {
  return <Provider store={store}><Counter /></Provider>
}

// Counter.tsx — sử dụng
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from './store'
import { increment } from './store/counterSlice'

function Counter() {
  const count = useSelector((state: RootState) => state.counter.value)
  const dispatch = useDispatch()
  return <button onClick={() => dispatch(increment())}>{count}</button>
}

Tương đương trong Zustand:

// store/useCounter.ts
import { create } from 'zustand'

interface CounterStore {
  value: number
  increment: () => void
  decrement: () => void
  incrementByAmount: (amount: number) => void
}

export const useCounter = create<CounterStore>((set) => ({
  value: 0,
  increment: () => set((state) => ({ value: state.value + 1 })),
  decrement: () => set((state) => ({ value: state.value - 1 })),
  incrementByAmount: (amount) => set((state) => ({ value: state.value + amount })),
}))

// Counter.tsx — sử dụng (không cần Provider, không cần boilerplate)
import { useCounter } from './store/useCounter'

function Counter() {
  const { value, increment } = useCounter()
  return <button onClick={increment}>{value}</button>
}

Cách tiếp cận của RTK không tệ — nếu bạn đã dùng một thời gian, slice pattern đọc rất mạch lạc và dễ đoán. Điểm ma sát nằm ở chỗ: cần setup Provider, phải khai báo typed dispatch/selector, và phải biết trước hình dạng của store trước khi viết được một dòng UI. Zustand gộp tất cả lại — store và các action nằm trong một file, import hook vào là dùng ngay.

Async: thunks vs inline actions

RTK với một data fetch:

// store/userSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

export const fetchUser = createAsyncThunk('user/fetch', async (id: string) => {
  const res = await fetch(`/api/users/${id}`)
  return res.json()
})

const userSlice = createSlice({
  name: 'user',
  initialState: { data: null, loading: false, error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => { state.loading = true })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.loading = false
        state.data = action.payload
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.loading = false
        state.error = action.error.message
      })
  },
})

Zustand với một data fetch:

// store/useUser.ts
import { create } from 'zustand'

interface UserStore {
  data: User | null
  loading: boolean
  error: string | null
  fetchUser: (id: string) => Promise<void>
}

export const useUser = create<UserStore>((set) => ({
  data: null,
  loading: false,
  error: null,
  fetchUser: async (id) => {
    set({ loading: true, error: null })
    try {
      const res = await fetch(`/api/users/${id}`)
      set({ data: await res.json(), loading: false })
    } catch (err) {
      set({ error: (err as Error).message, loading: false })
    }
  },
}))

Zustand viết async function thẳng trong định nghĩa store — không cần thunk factory, không cần extraReducers, không cần builder DSL. Cả hai đều ổn. Pattern của RTK mạnh hơn khi cần middleware (logging, analytics, điều phối side-effect). Pattern của Zustand nhanh hơn cho trường hợp thông thường.

Một lưu ý về data fetching: Zustand lẫn RTK đều không phải công cụ phù hợp cho server state. RTK Query xử lý caching, revalidation, và deduplication — kết hợp với Zustand cho client-side UI state nếu bạn muốn tận dụng cả hai. Zustand cũng kết hợp tốt với TanStack Query cho cùng mục đích phân tách đó. Nếu bạn đang cân nhắc API layer cho app React, xem tRPC vs GraphQL để chọn đúng công cụ bổ sung.

Bảng so sánh

Zustand v5Redux Toolkit v2
Bundle (min+gz)~2.1 KB~15–19 KB
Parse time (4× throttle)8 ms34 ms
Boilerplate (counter)~15 dòng~50+ dòng
DevToolsCó (qua middleware)Có (tích hợp sẵn)
Time-travel debuggingKhông
Pattern asyncInline asynccreateAsyncThunk
Server stateKhông cóRTK Query
Yêu cầu React 18+
TypeScript ergonomicsXuất sắc — generics tự suyTốt — PayloadAction<T> tường minh
Setup testĐơn giảnTrung bình (cần Provider wrapping)
Hỗ trợ middlewareCó (mạnh mẽ)

DevTools và time-travel

RTK mạnh hơn ở mảng DevTools. Redux DevTools Extension cho bạn toàn bộ action history, time-travel, và khả năng so sánh state ở bất kỳ thời điểm nào. Zustand hỗ trợ DevTools qua import { devtools } from 'zustand/middleware', nhưng trải nghiệm nông hơn — bạn thấy state snapshot, không thấy action stream.

Nếu team bạn dựa vào time-travel để debug UI state, khoảng cách đó có thực. Còn nếu bạn hiếm khi mở DevTools cho state — thay vào đó dùng component-level logging hoặc network trace — thì không quan trọng.

TypeScript ergonomics

Cả hai đều tốt. Zustand v5 đã bỏ dependency cứng vào use-sync-external-store và generic inference sạch hơn — kiểu dữ liệu của store chảy xuyên suốt mà không cần cast thủ công RootState. RTK có PayloadAction<T> tường minh nhưng dễ đoán.

Với monorepo lớn có nhiều engineer, cái verbose của RTK lại trở thành điểm mạnh — cấu trúc bắt buộc pattern dễ audit và review. Zustand khó bị dùng sai ban đầu, nhưng lại cho bạn đủ dây để tự làm khó mình nếu nhét quá nhiều logic vào một store.

Khi nào nên giữ Redux

Đây là dấu hiệu cụ thể, không phải nguyên tắc trừu tượng:

  • Có hơn 5 engineer chạm vào state mỗi ngày. Cấu trúc bắt buộc của RTK (slice → reducer → action) là công cụ phối hợp nhóm. Zustand store linh hoạt, nghĩa là mỗi người sẽ định nghĩa theo cách riêng.
  • Bạn dùng time-travel debugging thường xuyên. Không có gì trong Zustand sánh được với Redux DevTools action replay cho workflow đó.
  • Bạn đang dùng RTK Query và nó chạy tốt. RTK Query xử lý cache invalidation, optimistic update, và tag-based refetch. Nó đã trưởng thành và giải quyết vấn đề thực. Đừng tháo ra.
  • Bạn có yêu cầu compliance về state observability. Action log là audit trail. Một số ứng dụng trong lĩnh vực được quản lý chặt cần điều này.
  • Bạn có codebase RTK hiện tại không có vấn đề lớn. Chi phí migrate có thực. Viết lại toàn bộ state management trên một app lớn mất nhiều tuần mà không tạo ra giá trị nào cho người dùng.

Khi nào nên chuyển sang Zustand

  • Dự án mới, team 1–3 người. Không lý do gì phải chịu overhead của RTK khi có thể bắt đầu gọn nhẹ.
  • Bạn build widget, plugin, hoặc app nhúng. Khoảng cách 2.1 KB vs 15–19 KB có ý nghĩa thực khi bạn không kiểm soát host page.
  • Setup RTK hiện tại cảm giác như gánh nặng. Nếu bạn phải viết ba file để thêm một tính năng, ma sát đó cộng dồn. Zustand xóa bỏ điều đó.
  • State chủ yếu là UI state, không phải server state. Nếu bạn quản lý modal, tab, drawer open/close, local filter — Zustand là công cụ đúng. Dùng TanStack Query hoặc RTK Query cho data fetching dù bạn chọn thư viện nào.
  • Bạn muốn không cần Provider. Zustand store hoạt động như plain module import. Không context, không Provider wrapping, không phụ thuộc vào component tree.

Kết luận

Dự án mới: Dùng Zustand. Bundle nhỏ hơn, API ngắn hơn, và bạn có thể thêm cấu trúc khi app lớn dần. Nếu cần server state mạnh, thêm TanStack Query bên cạnh.

Dự án RTK hiện tại: Đừng migrate chỉ vì muốn migrate. RTK v2 rất chắc chắn. API combineSlices và inline selector thêm vào từ RTK 2.0, cùng autoBatchEnhancer (ra mắt từ 1.9.x, bật mặc định từ 2.0), đã xử lý hầu hết các điểm khó chịu cũ. Nếu nó không làm bạn đau, hãy để yên.

Enterprise / team lớn: RTK vẫn xứng đáng. Cấu trúc tường minh và DevTools đáng giá overhead khi 10+ engineer cùng đóng góp vào shared store và bạn cần audit trail.

Câu chuyện “Zustand đang thắng” trên npm trends là có thật — nhưng nó phản ánh sự thay đổi trong việc ai đang bắt đầu dự án mới, không phải bằng chứng rằng RTK sai. Chúng giải quyết vấn đề hơi khác nhau. Chọn cái phù hợp với ràng buộc của bạn, không phải cái có nhiều GitHub star hơn. Nếu bạn đang cân nhắc lựa chọn framework, React vs Svelte 2026 sẽ giúp bạn có bức tranh đầy đủ hơn.

Tài nguyên

Nếu bạn muốn đi sâu hơn về các pattern quản lý state:

Lưu ý

  • Đo lường parse time (Zustand 8 ms, RTK 34 ms) được thực hiện với 4× CPU throttle qua Chrome DevTools (Chrome 124, macOS 14.5, MacBook Pro 14-in M3 Pro). Trên máy developer hiện đại, cả hai đều không cảm nhận được.
  • Kích thước bundle lấy từ Bundlephobia tại thời điểm viết bài. Tree-shaking RTK giúp giảm xuống còn 10–12 KB nếu bạn chỉ import những gì cần.
  • Bài viết này có chứa affiliate link đến Frontend Masters và Scrimba. Toolchew đã tự test cả hai trước khi gắn link. Kết luận phía trên không bị ảnh hưởng bởi mối quan hệ affiliate — RTK Query không có affiliate mà chúng tôi vẫn khuyên giữ lại nếu nó đang hoạt động tốt.
  • Chúng tôi chưa test React Native, vốn có câu trả lời phức tạp hơn (middleware story của RTK đôi khi phù hợp hơn cho các background sync pattern trên mobile).