Giới thiệu chung về State Management trong React

Trong các ứng dụng React hiện đại, việc quản lý trạng thái (state) không chỉ là việc lưu trữ dữ liệu mà còn liên quan tới cách dữ liệu được chia sẻ, cập nhật và tối ưu hoá render. Khi dự án mở rộng, lựa chọn một giải pháp state management phù hợp có thể giảm thiểu bugs, cải thiện hiệu năng và tăng tốc độ phát triển. Bài viết sẽ đi sâu vào ba thư viện phổ biến: Redux Toolkit, ZustandJotai, đồng thời trình bày cách tích hợp chúng với TypeScript để đạt được code an toàn và dễ bảo trì.

Tiêu chuẩn đánh giá một giải pháp State Management

Trước khi so sánh, chúng ta cần xác định các tiêu chí sau:

  • Độ phức tạp API: Cách thức khai báo store, slice, hoặc atom.
  • Hiệu năng render: Số lần render không cần thiết và khả năng memoization.
  • Khả năng mở rộng: Hỗ trợ modular, code splitting và lazy loading.
  • Hỗ trợ TypeScript: Độ mạnh của typings, khả năng infer kiểu tự động.
  • Độ phổ biến & cộng đồng: Tài liệu, plugin, công cụ devtools.

Redux Toolkit – Định hướng “bảo toàn” cho dự án lớn

Cấu trúc cơ bản

Redux Toolkit (RTK) là wrapper chính thức của Redux, cung cấp các API như createSlice, configureStorecreateAsyncThunk. Khi kết hợp với TypeScript, RTK tự động tạo ra các kiểu cho state và action, giảm thiểu boilerplate.

import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
  value: number;
}

const initialState: CounterState = { value: 0 };

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value += 1;
    },
    decrement(state) {
      state.value -= 1;
    },
    addBy(state, action: PayloadAction) {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, addBy } = counterSlice.actions;

export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;

Trong ví dụ trên, PayloadAction<number> giúp TypeScript suy ra kiểu payload cho action addBy. Khi sử dụng trong component, chúng ta có thể viết:

import { useAppDispatch, useAppSelector } from '../hooks';
import { increment, addBy } from '../store/counterSlice';

function Counter() {
  const dispatch = useAppDispatch();
  const value = useAppSelector(state => state.counter.value);

  return (
    <div>
      <h3>Value: {value}</h3>
      <button onClick={() => dispatch(increment())}>+ dispatch(addBy(5))}>Add 5</button>
    </div>
  );
}

Ưu điểm

  • Kiểm soát toàn cục mạnh mẽ: Tất cả state nằm trong một store duy nhất, dễ dàng debug bằng Redux DevTools.
  • Typed API: RTK cung cấp các kiểu đã được infer, giảm lỗi runtime.
  • Middleware tích hợp: Thư viện như redux-thunkredux-saga có thể thêm vào mà không thay đổi cấu trúc.

Nhược điểm

  • Boilerplate vẫn còn: Dù giảm so với Redux truyền thống, nhưng vẫn cần tạo slice, reducer và selector.
  • Chi phí render: Khi state toàn cục lớn, mỗi thay đổi có thể gây render lại nhiều component nếu không memoize đúng.

Zustand – Giải pháp nhẹ, “hook‑first”

Cách tạo store

Zustand dựa trên hook, cho phép tạo store dưới dạng một hàm trả về state và các mutator. Với TypeScript, chúng ta khai báo kiểu cho state và các hàm cập nhật.

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

interface CounterState {
  count: number;
  inc: () => void;
  dec: () => void;
  add: (n: number) => void;
}

export const useCounterStore = create()(
  devtools(
    persist(
      (set) => ({
        count: 0,
        inc: () => set(state => ({ count: state.count + 1 })),
        dec: () => set(state => ({ count: state.count - 1 })),
        add: (n) => set(state => ({ count: state.count + n })),
      }),
      { name: 'counter-storage' }
    )
  )
);

Component sử dụng store:

function Counter() {
  const { count, inc, add } = useCounterStore();
  return (
    <div>
      <h3>Count: {count}</h3>
      <button onClick={inc}>+ add(5)}>Add 5</button>
    </div>
  );
}

Ưu điểm

  • Độ nhẹ: Thư viện chỉ ~1KB gzipped, không yêu cầu Provider.
  • Hook‑centric: Dễ dàng tích hợp trong component mà không cần viết selector.
  • Middleware tùy chọn: Devtools, persist, immer đều có sẵn.

Nhược điểm

  • Quản lý state toàn cục không có cấu trúc module như Redux, có thể gây khó khăn khi dự án lớn.
  • Không có DevTools mạnh như Redux (mặc dù có plugin, nhưng ít tính năng).

Jotai – Atom‑based, tối ưu cho granular state

Khái niệm Atom

Jotai giới thiệu atom – một đơn vị state độc lập. Mỗi atom có thể được tiêu thụ trong bất kỳ component nào, và chỉ những component sử dụng atom đó mới re‑render khi giá trị thay đổi.

import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const addAtom = atom(
  (get) => get(countAtom),
  (get, set, value: number) => set(countAtom, get(countAtom) + value)
);

Component:

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [, add] = useAtom(addAtom);

  return (
    <div>
      <h3>Count: {count}</h3>
      <button onClick={() => setCount(c => c + 1)}>+ add(5)}>Add 5</button>
    </div>
  );
}

Ưu điểm

  • Granular re‑render: Chỉ component tiêu thụ atom thay đổi mới render.
  • Đơn giản, không cần Provider: Jotai tự động tạo context nội bộ.
  • Hỗ trợ async atom: Dễ dàng quản lý dữ liệu bất đồng bộ.

Nhược điểm

  • Quản lý số lượng atom khi dự án lớn có thể trở nên phức tạp.
  • Không có ecosystem rộng như Redux (middleware, devtools).

So sánh chi tiết

Tiêu chí Redux Toolkit Zustand Jotai
Độ nhẹ ~10KB gzipped ~1KB gzipped ~2KB gzipped
Kiểu API Slice, thunk, selector Hook + set Atom + hook
DevTools Redux DevTools (full) Zustand DevTools (basic) Jotai DevTools (experimental)
TypeScript support Strong inference Good inference Good inference
Quy mô dự án Lớn, enterprise Nhỏ‑trung Nhỏ‑trung, micro‑frontend

Best Practices khi lựa chọn và triển khai

1. Đánh giá độ phức tạp dự án

Nếu dự án có nhiều tính năng, đa module, cần debug chi tiết, Redux Toolkit là lựa chọn an toàn. Đối với MVP hoặc dự án nhanh, Zustand hoặc Jotai giúp giảm thời gian thiết lập.

2. Sử dụng memoization đúng chỗ

Trong Redux, sử dụng createSelector (reselect) để tránh render lại không cần thiết. Với Zustand, shallow compare hoặc useStore selector giúp tối ưu. Jotai tự động memoize atom, nhưng khi tạo derived atom, nên dùng atomWithCache để tránh tính toán lại.

3. Tích hợp TypeScript một cách nhất quán

Đặt kiểu cho state ở một nơi (ví dụ src/types/store.ts) và import vào các slice/atom. Đối với Redux Toolkit, khai báo RootStateAppDispatch để dùng trong useSelectoruseDispatch. Với Zustand và Jotai, khai báo interface cho store/atom và dùng as const để giữ literal types.

4. Kiểm soát side‑effects

Redux Toolkit cung cấp createAsyncThunk và middleware để quản lý async logic. Với Zustand, bạn có thể viết async mutator trong set hoặc dùng middleware zustand/middleware. Jotai có atomWithPromise hoặc atomWithQuery để xử lý async.

5. Kiểm thử

Redux Toolkit: test slice reducer bằng expecttoEqual. Zustand: export store creator và kiểm tra state bằng getState(). Jotai: render hook với renderHook từ @testing-library/react-hooks và kiểm tra giá trị atom.

Kết luận

Việc lựa chọn giữa Redux Toolkit, Zustand và Jotai phụ thuộc vào quy mô dự án, yêu cầu về performance và mức độ kiểm soát state. Redux Toolkit vẫn là tiêu chuẩn cho các ứng dụng enterprise, trong khi Zustand và Jotai thích hợp cho các dự án nhẹ, micro‑frontend hoặc khi muốn giảm boilerplate. Để nắm vững cách tích hợp các giải pháp này cùng TypeScript, bạn có thể Tham khảo khóa học "Lập trình Front-End với ReactJS + TypeScript" tại đây.