Hướng dẫn sử dụng Zustand để quản lý State trong React: Gọn nhẹ và không Boilerplate

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Nỗi ngán ngẩm mang tên boilerplate của Redux

Làm việc với React từ vài nghìn dòng code trở lên, chắc hẳn bạn đã thấm thía cảnh chia sẻ state giữa các component. Dùng useState kết hợp prop drilling qua 4-5 tầng component trung gian vừa rối mắt vừa dễ bug. Đổi sang Context API thì mỗi lần state nhảy số, cả cụm component con lại re-render hàng loạt.

Nhiều team chọn Redux Toolkit như phản xạ tự nhiên. Redux chặt chẽ thật, nhưng đi kèm là hàng tá file action, reducer, selector và cấu hình Provider. Chỉ để làm tính năng toggle dark-mode hay lưu token đăng nhập, bạn phải viết thêm cả trăm dòng code râu ria.

Dùng búa tạ đập hạt dẻ là nguyên nhân khiến codebase phình to. Nhiều khi chúng ta chỉ cần một store toàn cục: nhẹ, không bắt bọc Provider khắp cây DOM và chỉ re-render đúng nơi cần dữ liệu. Đó là lý do mình chuyển hẳn sang Zustand cho hầu hết dự án gần đây.

Zustand hoạt động dưới góc nhìn kỹ thuật ra sao?

Được phát triển bởi nhóm Poimandres (tác giả của React Three Fiber), Zustand chọn hướng tiếp cận thực dụng dựa trên cơ chế hook:

  • Store thực chất là một Custom Hook: Bạn gom toàn bộ state và action cập nhật vào chung một hàm. Cần dữ liệu ở đâu, bạn chỉ việc gọi hook đó ra dùng.
  • Xóa bỏ hoàn toàn Context Provider: Không cần bọc <Provider> quanh component <App />. Cây component sạch hơn hẳn, viết unit test cũng nhàn hơn vì component không còn phụ thuộc context cha.
  • Selective Re-rendering tối ưu: Component chỉ render lại khi giá trị selector bạn đăng ký thực sự thay đổi. Tránh được hoàn toàn nhược điểm render lan truyền của Context API.
  • Bundle size vỏn vẹn ~1.1KB: Nhẹ hơn gấp nhiều lần so với dung lượng hàng chục KB của Redux Toolkit.

Thực hành từng bước với Zustand

1. Cài đặt thư viện

Chạy lệnh sau trong terminal dự án (Vite hoặc Next.js):

npm install zustand
# hoặc nếu dùng pnpm:
pnpm add zustand

2. Khởi tạo Store

Tạo file src/stores/useCartStore.js để quản lý giỏ hàng. State và hàm cập nhật nằm gọn trong một chỗ:

import { create } from 'zustand';

export const useCartStore = create((set) => ({
  items: [],
  totalCount: 0,

  // Action thêm sản phẩm
  addItem: (product) => set((state) => ({
    items: [...state.items, product],
    totalCount: state.totalCount + 1
  })),

  // Action làm trống giỏ hàng
  clearCart: () => set({ items: [], totalCount: 0 })
}));

Hàm set tự động shallow merge ở tầng root. Bạn chỉ cần truyền các field cần cập nhật mà không phải clone lại toàn bộ state ban đầu.

3. Gọi Store trong Component và chặn re-render thừa

Để tối ưu hiệu năng, hãy luôn trỏ đúng selector thay vì bốc nguyên cả store vào component:

import React from 'react';
import { useCartStore } from '../stores/useCartStore';

// Badge giỏ hàng: chỉ render lại khi totalCount thay đổi
export const CartBadge = () => {
  const totalCount = useCartStore((state) => state.totalCount);
  return <span className="badge">Giỏ hàng: {totalCount}</span>;
};

// Card sản phẩm: chỉ lấy hàm addItem, hoàn toàn miễn nhiễm với thay đổi của totalCount
export const ProductItem = ({ product }) => {
  const addItem = useCartStore((state) => state.addItem);

  return (
    <div className="product-card">
      <h4>{product.name}</h4>
      <p>Giá: {product.price.toLocaleString('vi-VN')}đ</p>
      <button onClick={() => addItem(product)}>Thêm vào giỏ</button>
    </div>
  );
};

Mỗi khi bấm ‘Thêm vào giỏ’, totalCount tăng lên. Chỉ CartBadge chạy lại render. ProductItem không hề bị render lại vì nó chỉ subscribe vào function addItem.

4. Xử lý tác vụ bất đồng bộ (Async Actions)

Quên Redux Thunk hay Redux Saga đi. Trong Zustand, bạn khai báo hàm async trực tiếp ngay trong store:

export const useUserStore = create((set) => ({
  userData: null,
  isLoading: false,
  error: null,

  fetchUserProfile: async (userId) => {
    set({ isLoading: true, error: null });
    try {
      const res = await fetch(`https://api.example.com/users/${userId}`);
      if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
      const data = await res.json();
      set({ userData: data, isLoading: false });
    } catch (err) {
      set({ error: err.message, isLoading: false });
    }
  }
}));

Mẹo nhỏ: Khi mockup schema hoặc kiểm tra response JSON từ API trước khi đưa vào store, bạn có thể format nhanh qua các trang web nhẹ như toolcraft.app thay vì phải mở DevTools chỉnh tay.

5. Tự động lưu LocalStorage với middleware persist

Muốn giữ trạng thái theme hay giỏ hàng khi người dùng F5? Bọc store với middleware persist là xong:

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useSettingsStore = create(
  persist(
    (set) => ({
      theme: 'light',
      toggleTheme: () => set((state) => ({
        theme: state.theme === 'light' ? 'dark' : 'light'
      }))
    }),
    {
      name: 'app-settings-storage', // key lưu trữ trong localStorage
    }
  )
);

Lời kết

Zustand mang lại trải nghiệm viết code nhẹ nhàng hơn hẳn so với sự cồng kềnh của Redux. Bạn giữ được cấu trúc gọn gàng của React hook mà vẫn kiểm soát chặt chẽ từng lượt re-render. Ở dự án tiếp theo, hãy thử cài Zustand một lần để cảm nhận sự khác biệt.

Share: