React & Frontend⏱ 11 phút đọc · 7 thg 10, 2026

Zustand là gì: quản lý state React gọn nhẹ, từ store đầu tiên tới Next.js

Zustand là gì và dùng thế nào cho đúng trong app thật? Bài này đi qua Zustand v5 với TypeScript: selector và re-render, useShallow, slices, persist, devtools, dùng store ngoài React, lưu ý với Next.js, và khi nào chọn Context, Redux Toolkit hay TanStack Query.

HOLETEX · POST
ZUSTAND
state React gọn nhẹ

Bạn làm trang bán khóa học. Header có icon giỏ hàng hiện số lượng, trang chi tiết có nút "Thêm vào giỏ", drawer bên phải liệt kê sản phẩm, trang checkout tính tổng tiền. Bốn component ở bốn nhánh khác nhau của cây, cùng cần một dữ liệu. Bạn bọc app trong một CartContext, chạy ổn, rồi mở React DevTools bật highlight re-render: mỗi lần mở drawer, cả header lẫn trang sản phẩm đều nháy theo.

Đây là lúc nhiều team tìm tới Zustand. Bài Quản lý state trong React đã giới thiệu Zustand ở mức "nó là gì". Bài này đi sâu hơn: viết store bằng TypeScript cho đúng, hiểu vì sao component re-render, chia store lớn, lưu giỏ hàng vào localStorage, debug bằng DevTools, gọi store từ ngoài React, và những cái bẫy khi dùng với Next.js.

Zustand là gì

Zustand là thư viện quản lý state cho React do nhóm pmndrs phát triển (cùng nhóm làm Jotai, React Three Fiber). Docs tự mô tả nó là giải pháp quản lý state "nhỏ, nhanh và scalable", API dựa trên hook.

Ba ý tưởng cốt lõi:

  • Store là một hook. create trả về một hook, component nào cần thì gọi, không phải bọc Provider quanh app.
  • Component chọn đúng phần state nó cần bằng selector, và chỉ re-render khi phần đó đổi.
  • Store sống ngoài React. Bạn đọc và ghi state từ bất kỳ đâu: event handler, hàm tiện ích, interceptor của API client.

Phiên bản hiện tại là Zustand v5 (bản mới nhất trên npm lúc viết bài là 5.0.15). v5 yêu cầu React 18 trở lên và TypeScript 4.5 trở lên, bỏ default export và bỏ việc truyền hàm so sánh (equality function) làm tham số thứ hai cho hook. Nếu bạn đọc tutorial cũ thấy useStore(selector, shallow), đó là cú pháp v4. Nếu vẫn cần hàm so sánh tùy chỉnh, v5 có createWithEqualityFn trong zustand/traditional.

bash
npm install zustand

Store đầu tiên với TypeScript: giỏ hàng

Với TypeScript, Zustand khuyên viết create<T>()(...), chú ý cặp ngoặc () thừa ở giữa. Lý do: kiểu state vừa được "trả ra" (giá trị khởi tạo) vừa được "nhận vào" (qua get), TypeScript không tự suy ra được, nên bạn khai báo kiểu tường minh. Cặp ngoặc thừa giúp các middleware phía sau vẫn suy ra kiểu đúng.

ts
// src/stores/cart-store.ts
import { create } from 'zustand'

export type CartItem = {
  id: string
  name: string
  price: number
  quantity: number
}

type CartState = {
  items: CartItem[]
  isOpen: boolean
}

type CartActions = {
  addItem: (product: Omit<CartItem, 'quantity'>) => void
  removeItem: (id: string) => void
  setQuantity: (id: string, quantity: number) => void
  toggleCart: () => void
  clear: () => void
}

export type CartStore = CartState & CartActions

export const useCartStore = create<CartStore>()((set, get) => ({
  items: [],
  isOpen: false,

  addItem: (product) =>
    set((state) => {
      const existing = state.items.find((item) => item.id === product.id)
      if (existing) {
        return {
          items: state.items.map((item) =>
            item.id === product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item,
          ),
        }
      }
      return { items: [...state.items, { ...product, quantity: 1 }] }
    }),

  removeItem: (id) =>
    set((state) => ({ items: state.items.filter((item) => item.id !== id) })),

  setQuantity: (id, quantity) => {
    if (quantity <= 0) {
      get().removeItem(id)
      return
    }
    set((state) => ({
      items: state.items.map((item) =>
        item.id === id ? { ...item, quantity } : item,
      ),
    }))
  },

  toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
  clear: () => set({ items: [] }),
}))

Hai hàm bạn dùng nhiều nhất:

  • set cập nhật state. Nó merge nông một cấp: set({ items: [] }) chỉ thay items, giữ nguyên isOpen. Nhưng nếu state có object lồng nhau, bạn phải tự spread từng tầng bên trong, giống this.setState của class component ngày trước.
  • get đọc state hiện tại ngay trong action. Hữu ích khi một action cần gọi action khác, như setQuantity gọi removeItem ở trên.

Quy tắc bất biến (immutable) vẫn áp dụng: luôn tạo mảng/object mới, không push thẳng vào state.items. Nếu thấy spread lồng nhau quá rối, Zustand có middleware immer, nhưng cần cài thêm package immer.

Selector và re-render: phần quan trọng nhất

Cách dùng store quyết định app nhanh hay chậm. Nguyên tắc: mỗi component chỉ lấy đúng thứ nó cần.

tsx
// src/stores/cart-selectors.ts
import type { CartStore } from './cart-store'

export const selectTotalQuantity = (state: CartStore) =>
  state.items.reduce((sum, item) => sum + item.quantity, 0)

export const selectSubtotal = (state: CartStore) =>
  state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
tsx
// src/components/cart-badge.tsx
import { useCartStore } from '@/stores/cart-store'
import { selectTotalQuantity } from '@/stores/cart-selectors'

export function CartBadge() {
  const count = useCartStore(selectTotalQuantity)
  const toggleCart = useCartStore((state) => state.toggleCart)

  return (
    <button type="button" onClick={toggleCart}>
      Giỏ hàng ({count})
    </button>
  )
}

Zustand so sánh kết quả selector giữa hai lần bằng Object.is. count là số, nên khi bạn mở/đóng drawer (isOpen đổi), CartBadge không re-render. Action như toggleCart là hàm được tạo một lần khi khởi tạo store, tham chiếu không đổi, nên select riêng nó cũng không gây re-render.

Ba cách dùng hay gặp và hậu quả:

Cách viếtKết quả
useCartStore(selectTotalQuantity)Chỉ re-render khi tổng số lượng đổi
const { items, addItem } = useCartStore()Lấy cả store, re-render khi bất kỳ field nào đổi, kể cả isOpen
useCartStore((s) => ({ items: s.items, count: s.items.length }))Mỗi lần trả object mới, trong v5 có thể gây vòng lặp vô hạn

Dòng cuối là lỗi phổ biến nhất khi nâng cấp từ v4. Docs migration ghi rõ: selector trả về tham chiếu mới có thể gây lỗi "Maximum update depth exceeded". Cách sửa là bọc bằng useShallow, nó so sánh nông từng key/phần tử thay vì so sánh tham chiếu:

tsx
import { useShallow } from 'zustand/react/shallow'
import { useCartStore } from '@/stores/cart-store'

export function CartDrawer() {
  const { items, isOpen, removeItem } = useCartStore(
    useShallow((state) => ({
      items: state.items,
      isOpen: state.isOpen,
      removeItem: state.removeItem,
    })),
  )

  if (!isOpen) return null

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name} x {item.quantity}
          <button type="button" onClick={() => removeItem(item.id)}>
            Xóa
          </button>
        </li>
      ))}
    </ul>
  )
}

Gợi ý thực tế: selector trả về giá trị primitive (số, chuỗi, boolean) hoặc một field có sẵn trong state thì không cần useShallow. Khi cần gom nhiều field vào object/mảng, hoặc tính ra mảng mới (filter, map, Object.keys), hãy dùng useShallow. Nếu muốn hiểu sâu hơn về re-render trong React nói chung, xem bài Tối ưu performance React.

Slices: chia store khi app lớn

Một store chứa giỏ hàng, user, UI, bộ lọc... sẽ nhanh chóng thành file 500 dòng. Zustand có slices pattern: mỗi slice là một hàm tạo một phần state, rồi gộp lại thành một store.

ts
// src/stores/app-store.ts
import { create, type StateCreator } from 'zustand'

type CartSlice = {
  items: { id: string; quantity: number }[]
  addToCart: (id: string) => void
}

type UiSlice = {
  isCartOpen: boolean
  toggleCart: () => void
}

type CheckoutSlice = {
  checkout: () => void
}

type AppStore = CartSlice & UiSlice & CheckoutSlice

const createCartSlice: StateCreator<AppStore, [], [], CartSlice> = (set) => ({
  items: [],
  addToCart: (id) =>
    set((state) => ({ items: [...state.items, { id, quantity: 1 }] })),
})

const createUiSlice: StateCreator<AppStore, [], [], UiSlice> = (set) => ({
  isCartOpen: false,
  toggleCart: () => set((state) => ({ isCartOpen: !state.isCartOpen })),
})

// Slice dùng chung: gọi action của slice khác qua get()
const createCheckoutSlice: StateCreator<AppStore, [], [], CheckoutSlice> = (
  set,
  get,
) => ({
  checkout: () => {
    if (get().items.length === 0) return
    set({ items: [], isCartOpen: false })
  },
})

export const useAppStore = create<AppStore>()((...a) => ({
  ...createCartSlice(...a),
  ...createUiSlice(...a),
  ...createCheckoutSlice(...a),
}))

Hai lưu ý từ docs:

  • Middleware (persist, devtools) chỉ áp dụng ở store đã gộp, không áp trong từng slice.
  • Khi store có middleware, tham số thứ hai của StateCreator phải khai báo mutator tương ứng, ví dụ với devtools là StateCreator<AppStore, [['zustand/devtools', never]], [], CartSlice>.

Slices hay nhiều store nhỏ? Nếu các phần state ít liên quan nhau (giỏ hàng và theme), tách thành nhiều store riêng thường đơn giản hơn. Slices hợp khi các phần cần gọi lẫn nhau, như checkout vừa xóa giỏ vừa đóng drawer.

persist: giữ giỏ hàng sau khi F5

Người dùng thêm khóa học vào giỏ, lỡ tay đóng tab, quay lại thấy giỏ trống. Middleware persist giải quyết chuyện này, mặc định lưu vào localStorage. Kết hợp thêm devtools để debug:

ts
// src/stores/cart-store.ts (bản đầy đủ middleware)
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

// CartItem, CartStore khai báo như ở trên

export const useCartStore = create<CartStore>()(
  devtools(
    persist(
      (set) => ({
        items: [],
        isOpen: false,

        addItem: (product) =>
          set(
            (state) => {
              const existing = state.items.find((i) => i.id === product.id)
              if (existing) {
                return {
                  items: state.items.map((i) =>
                    i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i,
                  ),
                }
              }
              return { items: [...state.items, { ...product, quantity: 1 }] }
            },
            undefined,
            'cart/addItem',
          ),

        removeItem: (id) =>
          set(
            (state) => ({ items: state.items.filter((i) => i.id !== id) }),
            undefined,
            'cart/removeItem',
          ),

        setQuantity: (id, quantity) =>
          set(
            (state) => ({
              items:
                quantity <= 0
                  ? state.items.filter((i) => i.id !== id)
                  : state.items.map((i) => (i.id === id ? { ...i, quantity } : i)),
            }),
            undefined,
            'cart/setQuantity',
          ),

        toggleCart: () =>
          set((state) => ({ isOpen: !state.isOpen }), undefined, 'cart/toggle'),

        clear: () => set({ items: [] }, undefined, 'cart/clear'),
      }),
      {
        name: 'holetex-cart', // key trong localStorage
        version: 1,
        partialize: (state) => ({ items: state.items }),
      },
    ),
    { name: 'CartStore', enabled: process.env.NODE_ENV !== 'production' },
  ),
)

Những điểm đáng chú ý:

  • partialize chọn field nào được lưu. Ở đây chỉ lưu items. Nếu lưu cả isOpen, người dùng F5 sẽ thấy drawer tự bật lên, không ai muốn vậy.
  • version đánh số cấu trúc dữ liệu đã lưu. Khi bạn đổi shape của CartItem, tăng version: bản lưu cũ không khớp version sẽ bị bỏ qua, trừ khi bạn viết hàm migrate để chuyển dữ liệu cũ sang dạng mới.
  • Không lưu dữ liệu nhạy cảm (token, thông tin cá nhân) vào localStorage. Docs persist cũng cảnh báo createJSONStorage không validate dữ liệu đọc ra, dữ liệu hỏng hoặc bị sửa tay sẽ đi thẳng vào state. Với dữ liệu quan trọng, hãy validate (ví dụ bằng Zod) trong storage tùy chỉnh.
  • Thứ tự middleware: docs khuyên đặt devtools ở ngoài cùng, để không middleware nào khác thay đổi setState trước nó.

Với devtools, tham số thứ ba của set là tên action hiển thị trong Redux DevTools extension. Đặt tên theo dạng slice/action giúp bạn đọc lịch sử thay đổi dễ như dùng Redux. Theo docs, devtools mặc định chỉ bật ở môi trường development. Để chắc chắn với mọi bundler (kể cả Next.js), nên đặt tường minh option enabled như trong code trên.

Dùng store ngoài React: getState, setState, subscribe

Hook do create trả về có gắn sẵn các hàm getState, setState, subscribe và getInitialState. Đây là điểm Context không làm được.

ts
// src/lib/auth.ts
import { useCartStore } from '@/stores/cart-store'

export async function logout() {
  await fetch('/api/logout', { method: 'POST' })
  // Đọc và gọi action mà không cần ở trong component
  useCartStore.getState().clear()
}

// Reset toàn bộ store về trạng thái ban đầu (hữu ích trong test)
export function resetCartStore() {
  useCartStore.setState(useCartStore.getInitialState(), true)
}

// Lắng nghe thay đổi, trả về hàm hủy đăng ký
// Demo: đăng ký ngay khi import module. Trong app thật, đăng ký trong useEffect
// hoặc lúc khởi động client, và gọi hàm trả về để hủy.
export const unsubscribeCartLogger = useCartStore.subscribe((state, prevState) => {
  if (state.items !== prevState.items) {
    console.info('Cart changed:', state.items.length, 'items')
  }
})

getState() chỉ đọc giá trị tại thời điểm gọi, không đăng ký re-render. Trong component, vẫn dùng hook với selector.

Zustand với Next.js App Router: cái bẫy lớn nhất

Store tạo bằng create ở cấp module là biến global. Trên trình duyệt, mỗi người dùng có một bản riêng nên không sao. Nhưng trên server Next.js, một process phục vụ nhiều request cùng lúc. Nếu bạn ghi dữ liệu của người dùng A vào store global trong lúc render trên server, request của người dùng B có thể thấy dữ liệu đó.

Docs Zustand cho Next.js đưa ra hai khuyến nghị:

  1. Không dùng store global. Docs khuyên tạo store mới cho mỗi request thay vì khai báo biến global. (Store chỉ chứa state thuần client như trạng thái drawer thì rủi ro thấp, nhưng dữ liệu theo người dùng thì phải per-request.)
  2. Server Component không đọc hay ghi store. Server Component không dùng được hook hay context, và không được thiết kế để giữ state. (Xem thêm bài React Server Components.)

Cách làm: dùng createStore từ zustand/vanilla tạo factory, rồi đưa store vào context trong một Client Component.

ts
// src/stores/cart-store.ts (phiên bản cho Next.js)
import { createStore } from 'zustand/vanilla'

export type CartItem = { id: string; name: string; price: number; quantity: number }
export type CartState = { items: CartItem[] }
export type CartActions = {
  addItem: (product: Omit<CartItem, 'quantity'>) => void
  clear: () => void
}
export type CartStore = CartState & CartActions

export const defaultCartState: CartState = { items: [] }

export const createCartStore = (initState: CartState = defaultCartState) =>
  createStore<CartStore>()((set) => ({
    ...initState,
    addItem: (product) =>
      set((state) => {
        const existing = state.items.find((i) => i.id === product.id)
        if (existing) {
          return {
            items: state.items.map((i) =>
              i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i,
            ),
          }
        }
        return { items: [...state.items, { ...product, quantity: 1 }] }
      }),
    clear: () => set({ items: [] }),
  }))
tsx
// src/providers/cart-store-provider.tsx
'use client'

import { createContext, useContext, useState, type ReactNode } from 'react'
import { useStore } from 'zustand'
import { createCartStore, type CartState, type CartStore } from '@/stores/cart-store'

type CartStoreApi = ReturnType<typeof createCartStore>

const CartStoreContext = createContext<CartStoreApi | null>(null)

export function CartStoreProvider({
  children,
  initialState,
}: {
  children: ReactNode
  initialState?: CartState
}) {
  // useState với hàm khởi tạo: store chỉ được tạo một lần cho mỗi lần mount
  const [store] = useState(() => createCartStore(initialState))
  return <CartStoreContext.Provider value={store}>{children}</CartStoreContext.Provider>
}

export function useCartStore<T>(selector: (state: CartStore) => T): T {
  const store = useContext(CartStoreContext)
  if (!store) {
    throw new Error('useCartStore must be used within CartStoreProvider')
  }
  return useStore(store, selector)
}

Server Component (ví dụ layout.tsx) có thể lấy giỏ hàng của người dùng đã đăng nhập từ database rồi truyền vào initialState. Client Component bên dưới dùng useCartStore(selector) như bình thường. Selector và useShallow vẫn hoạt động giống hệt. Lưu ý: initialState chỉ được đọc một lần khi provider mount. Nếu dữ liệu server đổi sau đó (ví dụ sau router.refresh()), hãy cập nhật store qua action hoặc setState, đừng trông vào prop mới.

Đổi lại, bạn mất khả năng gọi useCartStore.getState() từ module bất kỳ, vì store giờ nằm trong context. Đây là đánh đổi có chủ đích.

Lưu ý với persist trong Next.js. Server không có localStorage, nên HTML render trên server luôn là giỏ trống, còn client đọc localStorage ra giỏ có hàng. Hai kết quả khác nhau dẫn tới lỗi hydration. Cách docs gợi ý: đặt skipHydration: true trong option của persist, rồi gọi rehydrate() sau khi component mount. Với store per-request ở trên, bạn bọc persist ngay trong factory:

ts
// src/stores/cart-store.ts (Next.js + persist)
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'

// CartItem, CartState, CartStore, defaultCartState khai báo như bản Next.js ở trên

export const createCartStore = (initState: CartState = defaultCartState) =>
  createStore<CartStore>()(
    persist(
      (set) => ({
        ...initState,
        addItem: (product) =>
          set((state) => {
            const existing = state.items.find((i) => i.id === product.id)
            if (existing) {
              return {
                items: state.items.map((i) =>
                  i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i,
                ),
              }
            }
            return { items: [...state.items, { ...product, quantity: 1 }] }
          }),
        clear: () => set({ items: [] }),
      }),
      {
        name: 'holetex-cart',
        partialize: (state) => ({ items: state.items }),
        skipHydration: true, // không tự đọc localStorage lúc tạo store
      },
    ),
  )

Rồi rehydrate trong provider, sau khi mount:

tsx
// src/providers/cart-store-provider.tsx (thêm rehydrate)
'use client'

import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { useStore } from 'zustand'
import { createCartStore, type CartState, type CartStore } from '@/stores/cart-store'

type CartStoreApi = ReturnType<typeof createCartStore>

const CartStoreContext = createContext<CartStoreApi | null>(null)

export function CartStoreProvider({
  children,
  initialState,
}: {
  children: ReactNode
  initialState?: CartState
}) {
  const [store] = useState(() => createCartStore(initialState))

  useEffect(() => {
    // Chỉ chạy trên client, sau lần render khớp với HTML từ server
    void store.persist.rehydrate()
  }, [store])

  return <CartStoreContext.Provider value={store}>{children}</CartStoreContext.Provider>
}

export function useCartStore<T>(selector: (state: CartStore) => T): T {
  const store = useContext(CartStoreContext)
  if (!store) {
    throw new Error('useCartStore must be used within CartStoreProvider')
  }
  return useStore(store, selector)
}

Nếu vừa có initialState từ server vừa có dữ liệu trong localStorage, mặc định persist sẽ merge nông và dữ liệu localStorage đè lên items. Hãy quyết định nguồn nào là chuẩn (thường là database với người đã đăng nhập) và dùng option merge của persist khi cần.

Docs Zustand ghi chú rằng hướng dẫn Next.js đang được cập nhật, nên trước khi áp dụng cho dự án thật, hãy đọc lại bản mới nhất trên trang docs.

Zustand, Context, Redux Toolkit hay TanStack Query?

Bốn công cụ này không thay thế hoàn toàn cho nhau. Câu hỏi đầu tiên luôn là: state này thuộc loại gì?

React ContextZustandRedux ToolkitTanStack Query
Loại stateClient, ít thay đổiClient stateClient state (+ server qua RTK Query)Server state (dữ liệu từ API)
Cài thêmKhôngCó, nhẹCó (@reduxjs/toolkit + react-redux)Có
ProviderBắt buộcKhông (trừ SSR per-request)Bắt buộcBắt buộc (QueryClientProvider)
Re-renderMọi consumer re-render khi value đổiTheo selectorTheo selectorTheo query
Quy ước cấu trúcTự đặtLinh hoạt, tự đặtChặt: slice, reducer, actionTheo query key
DevToolsReact DevToolsRedux DevTools qua middlewareRedux DevTools, hệ sinh thái lớnTanStack Query Devtools
Hợp vớiTheme, ngôn ngữ, user hiện tạiGiỏ hàng, UI state chia sẻ, wizard nhiều bướcApp lớn, team đông, cần quy ước chặtFetch, cache, refetch, mutation

Một vài nguyên tắc chọn nhanh:

  • Dữ liệu từ API (danh sách khóa học, profile) không nên tự nhét vào Zustand rồi tự lo loading, cache, refetch. Đó là việc của TanStack Query. Đọc bài React Query là gì để thấy phần này. Cách kết hợp phổ biến: TanStack Query cho server state, Zustand cho client state.
  • Giá trị ít đổi như theme, locale: Context là đủ, không cần thêm thư viện.
  • Team lớn, nhiều người, cần quy ước thống nhất và middleware phức tạp: Redux Toolkit vẫn là lựa chọn vững. Sự tự do của Zustand là ưu điểm với team nhỏ, nhưng với team đông có thể dẫn tới mỗi người viết store một kiểu, nên cần thống nhất quy ước từ đầu.
  • State client dùng chung giữa nhiều nhánh component, đổi thường xuyên: Zustand là lựa chọn hợp lý.

Sai lầm thường gặp

  • Gọi useCartStore() không selector. Component re-render với mọi thay đổi trong store. Luôn truyền selector.
  • Selector trả object/mảng mới mà không useShallow. Trong v5 có thể gây vòng lặp vô hạn. Bọc useShallow hoặc tách thành nhiều selector primitive.
  • Mutate state trực tiếp. state.items.push(x) rồi set({ items: state.items }) giữ nguyên tham chiếu, selector không phát hiện thay đổi, UI không cập nhật.
  • Quên spread object lồng nhau. set chỉ merge một cấp. set({ user: { name } }) sẽ xóa các field khác trong user.
  • Dùng store global trong Next.js để giữ dữ liệu theo người dùng trên server. Dùng store per-request qua context như ở trên.
  • Lưu server state vào Zustand. Bạn sẽ tự viết lại loading, error, cache invalidation, thứ TanStack Query đã làm tốt.
  • Persist mọi thứ. Dùng partialize để chỉ lưu đúng dữ liệu cần giữ, và tăng version khi đổi cấu trúc.

Tóm lại

Zustand cho bạn một store nằm ngoài React, đọc ghi được từ bất kỳ đâu, và component chỉ re-render khi phần nó chọn thay đổi. Sức mạnh nằm ở selector: dùng đúng thì app nhanh, dùng sai (lấy cả store, trả object mới) thì mất hết lợi thế. Với Next.js, nhớ hai điều: store per-request khi có dữ liệu theo người dùng, và Server Component không chạm vào store.

Nếu bạn muốn nắm chắc nền tảng React (state, re-render, hooks, kiến trúc component) để dùng Zustand, TanStack Query hay bất kỳ thư viện nào cho đúng thay vì copy theo tutorial, khóa React PRO của HoleTex đi từ nền tảng tới dự án thực tế.

Bài liên quan

Nguồn tham khảo: Zustand introduction (zustand.docs.pmnd.rs), Advanced TypeScript guide (zustand.docs.pmnd.rs), Prevent rerenders with useShallow (zustand.docs.pmnd.rs), Immutable state and merging (zustand.docs.pmnd.rs), Slices pattern (zustand.docs.pmnd.rs), persist (zustand.docs.pmnd.rs), Persisting store data (zustand.docs.pmnd.rs), devtools (zustand.docs.pmnd.rs), create (zustand.docs.pmnd.rs), Setup with Next.js (zustand.docs.pmnd.rs), Migrating to v5 (zustand.docs.pmnd.rs), Zustand releases (github.com), useContext (react.dev), Redux Toolkit (redux-toolkit.js.org). Cập nhật 2026-10-08.

Thấy hay? Chia sẻ